1 // FILE IS GENERATED BY COMBINING THE SOURCES IN THE "classes" DIRECTORY SO DON'T MODIFY THIS FILE DIRECTLY 2 (function(win) { 3 var whiteSpaceRe = /^\s*|\s*$/g, 4 undef, isRegExpBroken = 'B'.replace(/A(.)|B/, '$1') === '$1'; 5 6 var tinymce = { 7 majorVersion : '3', 8 9 minorVersion : '5.4.1', 10 11 releaseDate : '2012-06-24', 12 13 _init : function() { 14 var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; 15 16 t.isOpera = win.opera && opera.buildNumber; 17 18 t.isWebKit = /WebKit/.test(ua); 19 20 t.isIE = !t.isWebKit && !t.isOpera && (/MSIE/gi).test(ua) && (/Explorer/gi).test(na.appName); 21 22 t.isIE6 = t.isIE && /MSIE [56]/.test(ua); 23 24 t.isIE7 = t.isIE && /MSIE [7]/.test(ua); 25 26 t.isIE8 = t.isIE && /MSIE [8]/.test(ua); 27 28 t.isIE9 = t.isIE && /MSIE [9]/.test(ua); 29 30 t.isGecko = !t.isWebKit && /Gecko/.test(ua); 31 32 t.isMac = ua.indexOf('Mac') != -1; 33 34 t.isAir = /adobeair/i.test(ua); 35 36 t.isIDevice = /(iPad|iPhone)/.test(ua); 37 38 t.isIOS5 = t.isIDevice && ua.match(/AppleWebKit\/(\d*)/)[1]>=534; 39 40 // TinyMCE .NET webcontrol might be setting the values for TinyMCE 41 if (win.tinyMCEPreInit) { 42 t.suffix = tinyMCEPreInit.suffix; 43 t.baseURL = tinyMCEPreInit.base; 44 t.query = tinyMCEPreInit.query; 45 return; 46 } 47 48 // Get suffix and base 49 t.suffix = ''; 50 51 // If base element found, add that infront of baseURL 52 nl = d.getElementsByTagName('base'); 53 for (i=0; i<nl.length; i++) { 54 v = nl[i].href; 55 if (v) { 56 // Host only value like http://site.com or http://site.com:8008 57 if (/^https?:\/\/[^\/]+$/.test(v)) 58 v += '/'; 59 60 base = v ? v.match(/.*\//)[0] : ''; // Get only directory 61 } 62 } 63 64 function getBase(n) { 65 if (n.src && /tiny_mce(|_gzip|_jquery|_prototype|_full)(_dev|_src)?.js/.test(n.src)) { 66 if (/_(src|dev)\.js/g.test(n.src)) 67 t.suffix = '_src'; 68 69 if ((p = n.src.indexOf('?')) != -1) 70 t.query = n.src.substring(p + 1); 71 72 t.baseURL = n.src.substring(0, n.src.lastIndexOf('/')); 73 74 // If path to script is relative and a base href was found add that one infront 75 // the src property will always be an absolute one on non IE browsers and IE 8 76 // so this logic will basically only be executed on older IE versions 77 if (base && t.baseURL.indexOf('://') == -1 && t.baseURL.indexOf('/') !== 0) 78 t.baseURL = base + t.baseURL; 79 80 return t.baseURL; 81 } 82 83 return null; 84 }; 85 86 // Check document 87 nl = d.getElementsByTagName('script'); 88 for (i=0; i<nl.length; i++) { 89 if (getBase(nl[i])) 90 return; 91 } 92 93 // Check head 94 n = d.getElementsByTagName('head')[0]; 95 if (n) { 96 nl = n.getElementsByTagName('script'); 97 for (i=0; i<nl.length; i++) { 98 if (getBase(nl[i])) 99 return; 100 } 101 } 102 103 return; 104 }, 105 106 is : function(o, t) { 107 if (!t) 108 return o !== undef; 109 110 if (t == 'array' && (o.hasOwnProperty && o instanceof Array)) 111 return true; 112 113 return typeof(o) == t; 114 }, 115 116 makeMap : function(items, delim, map) { 117 var i; 118 119 items = items || []; 120 delim = delim || ','; 121 122 if (typeof(items) == "string") 123 items = items.split(delim); 124 125 map = map || {}; 126 127 i = items.length; 128 while (i--) 129 map[items[i]] = {}; 130 131 return map; 132 }, 133 134 each : function(o, cb, s) { 135 var n, l; 136 137 if (!o) 138 return 0; 139 140 s = s || o; 141 142 if (o.length !== undef) { 143 // Indexed arrays, needed for Safari 144 for (n=0, l = o.length; n < l; n++) { 145 if (cb.call(s, o[n], n, o) === false) 146 return 0; 147 } 148 } else { 149 // Hashtables 150 for (n in o) { 151 if (o.hasOwnProperty(n)) { 152 if (cb.call(s, o[n], n, o) === false) 153 return 0; 154 } 155 } 156 } 157 158 return 1; 159 }, 160 161 162 trim : function(s) { 163 return (s ? '' + s : '').replace(whiteSpaceRe, ''); 164 }, 165 166 create : function(s, p, root) { 167 var t = this, sp, ns, cn, scn, c, de = 0; 168 169 // Parse : <prefix> <class>:<super class> 170 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 171 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 172 173 // Create namespace for new class 174 ns = t.createNS(s[3].replace(/\.\w+$/, ''), root); 175 176 // Class already exists 177 if (ns[cn]) 178 return; 179 180 // Make pure static class 181 if (s[2] == 'static') { 182 ns[cn] = p; 183 184 if (this.onCreate) 185 this.onCreate(s[2], s[3], ns[cn]); 186 187 return; 188 } 189 190 // Create default constructor 191 if (!p[cn]) { 192 p[cn] = function() {}; 193 de = 1; 194 } 195 196 // Add constructor and methods 197 ns[cn] = p[cn]; 198 t.extend(ns[cn].prototype, p); 199 200 // Extend 201 if (s[5]) { 202 sp = t.resolve(s[5]).prototype; 203 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 204 205 // Extend constructor 206 c = ns[cn]; 207 if (de) { 208 // Add passthrough constructor 209 ns[cn] = function() { 210 return sp[scn].apply(this, arguments); 211 }; 212 } else { 213 // Add inherit constructor 214 ns[cn] = function() { 215 this.parent = sp[scn]; 216 return c.apply(this, arguments); 217 }; 218 } 219 ns[cn].prototype[cn] = ns[cn]; 220 221 // Add super methods 222 t.each(sp, function(f, n) { 223 ns[cn].prototype[n] = sp[n]; 224 }); 225 226 // Add overridden methods 227 t.each(p, function(f, n) { 228 // Extend methods if needed 229 if (sp[n]) { 230 ns[cn].prototype[n] = function() { 231 this.parent = sp[n]; 232 return f.apply(this, arguments); 233 }; 234 } else { 235 if (n != cn) 236 ns[cn].prototype[n] = f; 237 } 238 }); 239 } 240 241 // Add static methods 242 t.each(p['static'], function(f, n) { 243 ns[cn][n] = f; 244 }); 245 246 if (this.onCreate) 247 this.onCreate(s[2], s[3], ns[cn].prototype); 248 }, 249 250 walk : function(o, f, n, s) { 251 s = s || this; 252 253 if (o) { 254 if (n) 255 o = o[n]; 256 257 tinymce.each(o, function(o, i) { 258 if (f.call(s, o, i, n) === false) 259 return false; 260 261 tinymce.walk(o, f, n, s); 262 }); 263 } 264 }, 265 266 createNS : function(n, o) { 267 var i, v; 268 269 o = o || win; 270 271 n = n.split('.'); 272 for (i=0; i<n.length; i++) { 273 v = n[i]; 274 275 if (!o[v]) 276 o[v] = {}; 277 278 o = o[v]; 279 } 280 281 return o; 282 }, 283 284 resolve : function(n, o) { 285 var i, l; 286 287 o = o || win; 288 289 n = n.split('.'); 290 for (i = 0, l = n.length; i < l; i++) { 291 o = o[n[i]]; 292 293 if (!o) 294 break; 295 } 296 297 return o; 298 }, 299 300 addUnload : function(f, s) { 301 var t = this, unload; 302 303 unload = function() { 304 var li = t.unloads, o, n; 305 306 if (li) { 307 // Call unload handlers 308 for (n in li) { 309 o = li[n]; 310 311 if (o && o.func) 312 o.func.call(o.scope, 1); // Send in one arg to distinct unload and user destroy 313 } 314 315 // Detach unload function 316 if (win.detachEvent) { 317 win.detachEvent('onbeforeunload', fakeUnload); 318 win.detachEvent('onunload', unload); 319 } else if (win.removeEventListener) 320 win.removeEventListener('unload', unload, false); 321 322 // Destroy references 323 t.unloads = o = li = w = unload = 0; 324 325 // Run garbarge collector on IE 326 if (win.CollectGarbage) 327 CollectGarbage(); 328 } 329 }; 330 331 function fakeUnload() { 332 var d = document; 333 334 function stop() { 335 // Prevent memory leak 336 d.detachEvent('onstop', stop); 337 338 // Call unload handler 339 if (unload) 340 unload(); 341 342 d = 0; 343 }; 344 345 // Is there things still loading, then do some magic 346 if (d.readyState == 'interactive') { 347 // Fire unload when the currently loading page is stopped 348 if (d) 349 d.attachEvent('onstop', stop); 350 351 // Remove onstop listener after a while to prevent the unload function 352 // to execute if the user presses cancel in an onbeforeunload 353 // confirm dialog and then presses the browser stop button 354 win.setTimeout(function() { 355 if (d) 356 d.detachEvent('onstop', stop); 357 }, 0); 358 } 359 }; 360 361 f = {func : f, scope : s || this}; 362 363 if (!t.unloads) { 364 // Attach unload handler 365 if (win.attachEvent) { 366 win.attachEvent('onunload', unload); 367 win.attachEvent('onbeforeunload', fakeUnload); 368 } else if (win.addEventListener) 369 win.addEventListener('unload', unload, false); 370 371 // Setup initial unload handler array 372 t.unloads = [f]; 373 } else 374 t.unloads.push(f); 375 376 return f; 377 }, 378 379 removeUnload : function(f) { 380 var u = this.unloads, r = null; 381 382 tinymce.each(u, function(o, i) { 383 if (o && o.func == f) { 384 u.splice(i, 1); 385 r = f; 386 return false; 387 } 388 }); 389 390 return r; 391 }, 392 393 explode : function(s, d) { 394 if (!s || tinymce.is(s, 'array')) { 395 return s; 396 } 397 398 return tinymce.map(s.split(d || ','), tinymce.trim); 399 }, 400 401 _addVer : function(u) { 402 var v; 403 404 if (!this.query) 405 return u; 406 407 v = (u.indexOf('?') == -1 ? '?' : '&') + this.query; 408 409 if (u.indexOf('#') == -1) 410 return u + v; 411 412 return u.replace('#', v + '#'); 413 }, 414 415 // Fix function for IE 9 where regexps isn't working correctly 416 // Todo: remove me once MS fixes the bug 417 _replace : function(find, replace, str) { 418 // On IE9 we have to fake $x replacement 419 if (isRegExpBroken) { 420 return str.replace(find, function() { 421 var val = replace, args = arguments, i; 422 423 for (i = 0; i < args.length - 2; i++) { 424 if (args[i] === undef) { 425 val = val.replace(new RegExp('\\$' + i, 'g'), ''); 426 } else { 427 val = val.replace(new RegExp('\\$' + i, 'g'), args[i]); 428 } 429 } 430 431 return val; 432 }); 433 } 434 435 return str.replace(find, replace); 436 } 437 438 }; 439 440 // Initialize the API 441 tinymce._init(); 442 443 // Expose tinymce namespace to the global namespace (window) 444 win.tinymce = win.tinyMCE = tinymce; 445 446 // Describe the different namespaces 447 448 })(window); 449 450 451 (function($, tinymce) { 452 var is = tinymce.is, attrRegExp = /^(href|src|style)$/i, undef; 453 454 // jQuery is undefined 455 if (!$ && window.console) { 456 return console.log("Load jQuery first!"); 457 } 458 459 // Stick jQuery into the tinymce namespace 460 tinymce.$ = $; 461 462 // Setup adapter 463 tinymce.adapter = { 464 patchEditor : function(editor) { 465 var fn = $.fn; 466 467 // Adapt the css function to make sure that the data-mce-style 468 // attribute gets updated with the new style information 469 function css(name, value) { 470 var self = this; 471 472 // Remove data-mce-style when set operation occurs 473 if (value) 474 self.removeAttr('data-mce-style'); 475 476 return fn.css.apply(self, arguments); 477 }; 478 479 // Apapt the attr function to make sure that it uses the data-mce- prefixed variants 480 function attr(name, value) { 481 var self = this; 482 483 // Update/retrive data-mce- attribute variants 484 if (attrRegExp.test(name)) { 485 if (value !== undef) { 486 // Use TinyMCE behavior when setting the specifc attributes 487 self.each(function(i, node) { 488 editor.dom.setAttrib(node, name, value); 489 }); 490 491 return self; 492 } else 493 return self.attr('data-mce-' + name); 494 } 495 496 // Default behavior 497 return fn.attr.apply(self, arguments); 498 }; 499 500 // Patch various jQuery functions to handle tinymce specific attribute and content behavior 501 // we don't patch the jQuery.fn directly since it will most likely break compatibility 502 // with other jQuery logic on the page. Only instances created by TinyMCE should be patched. 503 function patch(jq) { 504 // Patch some functions, only patch the object once 505 if (jq.css !== css) { 506 // Patch css/attr to use the data-mce- prefixed attribute variants 507 jq.css = css; 508 jq.attr = attr; 509 510 jq.tinymce = editor; 511 512 // Each pushed jQuery instance needs to be patched 513 // as well for example when traversing the DOM 514 jq.pushStack = function() { 515 return patch(fn.pushStack.apply(this, arguments)); 516 }; 517 } 518 519 return jq; 520 }; 521 522 // Add a $ function on each editor instance this one is scoped for the editor document object 523 // this way you can do chaining like this tinymce.get(0).$('p').append('text').css('color', 'red'); 524 editor.$ = function(selector, scope) { 525 var doc = editor.getDoc(); 526 527 return patch($(selector || doc, doc || scope)); 528 }; 529 } 530 }; 531 532 // Patch in core NS functions 533 tinymce.extend = $.extend; 534 tinymce.extend(tinymce, { 535 map : $.map, 536 grep : function(a, f) {return $.grep(a, f || function(){return 1;});}, 537 inArray : function(a, v) {return $.inArray(v, a || []);} 538 539 /* Didn't iterate stylesheets 540 each : function(o, cb, s) { 541 if (!o) 542 return 0; 543 544 var r = 1; 545 546 $.each(o, function(nr, el){ 547 if (cb.call(s, el, nr, o) === false) { 548 r = 0; 549 return false; 550 } 551 }); 552 553 return r; 554 }*/ 555 }); 556 557 // Patch in functions in various clases 558 // Add a "#ifndefjquery" statement around each core API function you add below 559 var patches = { 560 'tinymce.dom.DOMUtils' : { 561 /* 562 addClass : function(e, c) { 563 if (is(e, 'array') && is(e[0], 'string')) 564 e = e.join(',#'); 565 return (e && $(is(e, 'string') ? '#' + e : e) 566 .addClass(c) 567 .attr('class')) || false; 568 }, 569 570 hasClass : function(n, c) { 571 return $(is(n, 'string') ? '#' + n : n).hasClass(c); 572 }, 573 574 removeClass : function(e, c) { 575 if (!e) 576 return false; 577 578 var r = []; 579 580 $(is(e, 'string') ? '#' + e : e) 581 .removeClass(c) 582 .each(function(){ 583 r.push(this.className); 584 }); 585 586 return r.length == 1 ? r[0] : r; 587 }, 588 */ 589 590 select : function(pattern, scope) { 591 var t = this; 592 593 return $.find(pattern, t.get(scope) || t.get(t.settings.root_element) || t.doc, []); 594 }, 595 596 is : function(n, patt) { 597 return $(this.get(n)).is(patt); 598 } 599 600 /* 601 show : function(e) { 602 if (is(e, 'array') && is(e[0], 'string')) 603 e = e.join(',#'); 604 605 $(is(e, 'string') ? '#' + e : e).css('display', 'block'); 606 }, 607 608 hide : function(e) { 609 if (is(e, 'array') && is(e[0], 'string')) 610 e = e.join(',#'); 611 612 $(is(e, 'string') ? '#' + e : e).css('display', 'none'); 613 }, 614 615 isHidden : function(e) { 616 return $(is(e, 'string') ? '#' + e : e).is(':hidden'); 617 }, 618 619 insertAfter : function(n, e) { 620 return $(is(e, 'string') ? '#' + e : e).after(n); 621 }, 622 623 replace : function(o, n, k) { 624 n = $(is(n, 'string') ? '#' + n : n); 625 626 if (k) 627 n.children().appendTo(o); 628 629 n.replaceWith(o); 630 }, 631 632 setStyle : function(n, na, v) { 633 if (is(n, 'array') && is(n[0], 'string')) 634 n = n.join(',#'); 635 636 $(is(n, 'string') ? '#' + n : n).css(na, v); 637 }, 638 639 getStyle : function(n, na, c) { 640 return $(is(n, 'string') ? '#' + n : n).css(na); 641 }, 642 643 setStyles : function(e, o) { 644 if (is(e, 'array') && is(e[0], 'string')) 645 e = e.join(',#'); 646 $(is(e, 'string') ? '#' + e : e).css(o); 647 }, 648 649 setAttrib : function(e, n, v) { 650 var t = this, s = t.settings; 651 652 if (is(e, 'array') && is(e[0], 'string')) 653 e = e.join(',#'); 654 655 e = $(is(e, 'string') ? '#' + e : e); 656 657 switch (n) { 658 case "style": 659 e.each(function(i, v){ 660 if (s.keep_values) 661 $(v).attr('data-mce-style', v); 662 663 v.style.cssText = v; 664 }); 665 break; 666 667 case "class": 668 e.each(function(){ 669 this.className = v; 670 }); 671 break; 672 673 case "src": 674 case "href": 675 e.each(function(i, v){ 676 if (s.keep_values) { 677 if (s.url_converter) 678 v = s.url_converter.call(s.url_converter_scope || t, v, n, v); 679 680 t.setAttrib(v, 'data-mce-' + n, v); 681 } 682 }); 683 684 break; 685 } 686 687 if (v !== null && v.length !== 0) 688 e.attr(n, '' + v); 689 else 690 e.removeAttr(n); 691 }, 692 693 setAttribs : function(e, o) { 694 var t = this; 695 696 $.each(o, function(n, v){ 697 t.setAttrib(e,n,v); 698 }); 699 } 700 */ 701 } 702 703 /* 704 'tinymce.dom.Event' : { 705 add : function (o, n, f, s) { 706 var lo, cb; 707 708 cb = function(e) { 709 e.target = e.target || this; 710 f.call(s || this, e); 711 }; 712 713 if (is(o, 'array') && is(o[0], 'string')) 714 o = o.join(',#'); 715 o = $(is(o, 'string') ? '#' + o : o); 716 if (n == 'init') { 717 o.ready(cb, s); 718 } else { 719 if (s) { 720 o.bind(n, s, cb); 721 } else { 722 o.bind(n, cb); 723 } 724 } 725 726 lo = this._jqLookup || (this._jqLookup = []); 727 lo.push({func : f, cfunc : cb}); 728 729 return cb; 730 }, 731 732 remove : function(o, n, f) { 733 // Find cfunc 734 $(this._jqLookup).each(function() { 735 if (this.func === f) 736 f = this.cfunc; 737 }); 738 739 if (is(o, 'array') && is(o[0], 'string')) 740 o = o.join(',#'); 741 742 $(is(o, 'string') ? '#' + o : o).unbind(n,f); 743 744 return true; 745 } 746 } 747 */ 748 }; 749 750 // Patch functions after a class is created 751 tinymce.onCreate = function(ty, c, p) { 752 tinymce.extend(p, patches[c]); 753 }; 754 })(window.jQuery, tinymce); 755 756 757 758 tinymce.create('tinymce.util.Dispatcher', { 759 scope : null, 760 listeners : null, 761 inDispatch: false, 762 763 Dispatcher : function(scope) { 764 this.scope = scope || this; 765 this.listeners = []; 766 }, 767 768 add : function(callback, scope) { 769 this.listeners.push({cb : callback, scope : scope || this.scope}); 770 771 return callback; 772 }, 773 774 addToTop : function(callback, scope) { 775 var self = this, listener = {cb : callback, scope : scope || self.scope}; 776 777 // Create new listeners if addToTop is executed in a dispatch loop 778 if (self.inDispatch) { 779 self.listeners = [listener].concat(self.listeners); 780 } else { 781 self.listeners.unshift(listener); 782 } 783 784 return callback; 785 }, 786 787 remove : function(callback) { 788 var listeners = this.listeners, output = null; 789 790 tinymce.each(listeners, function(listener, i) { 791 if (callback == listener.cb) { 792 output = listener; 793 listeners.splice(i, 1); 794 return false; 795 } 796 }); 797 798 return output; 799 }, 800 801 dispatch : function() { 802 var self = this, returnValue, args = arguments, i, listeners = self.listeners, listener; 803 804 self.inDispatch = true; 805 806 // Needs to be a real loop since the listener count might change while looping 807 // And this is also more efficient 808 for (i = 0; i < listeners.length; i++) { 809 listener = listeners[i]; 810 returnValue = listener.cb.apply(listener.scope, args.length > 0 ? args : [listener.scope]); 811 812 if (returnValue === false) 813 break; 814 } 815 816 self.inDispatch = false; 817 818 return returnValue; 819 } 820 821 }); 822 823 (function() { 824 var each = tinymce.each; 825 826 tinymce.create('tinymce.util.URI', { 827 URI : function(u, s) { 828 var t = this, o, a, b, base_url; 829 830 // Trim whitespace 831 u = tinymce.trim(u); 832 833 // Default settings 834 s = t.settings = s || {}; 835 836 // Strange app protocol that isn't http/https or local anchor 837 // For example: mailto,skype,tel etc. 838 if (/^([\w\-]+):([^\/]{2})/i.test(u) || /^\s*#/.test(u)) { 839 t.source = u; 840 return; 841 } 842 843 // Absolute path with no host, fake host and protocol 844 if (u.indexOf('/') === 0 && u.indexOf('//') !== 0) 845 u = (s.base_uri ? s.base_uri.protocol || 'http' : 'http') + '://mce_host' + u; 846 847 // Relative path http:// or protocol relative //path 848 if (!/^[\w\-]*:?\/\//.test(u)) { 849 base_url = s.base_uri ? s.base_uri.path : new tinymce.util.URI(location.href).directory; 850 u = ((s.base_uri && s.base_uri.protocol) || 'http') + '://mce_host' + t.toAbsPath(base_url, u); 851 } 852 853 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 854 u = u.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 855 u = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(u); 856 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { 857 var s = u[i]; 858 859 // Zope 3 workaround, they use @@something 860 if (s) 861 s = s.replace(/\(mce_at\)/g, '@@'); 862 863 t[v] = s; 864 }); 865 866 b = s.base_uri; 867 if (b) { 868 if (!t.protocol) 869 t.protocol = b.protocol; 870 871 if (!t.userInfo) 872 t.userInfo = b.userInfo; 873 874 if (!t.port && t.host === 'mce_host') 875 t.port = b.port; 876 877 if (!t.host || t.host === 'mce_host') 878 t.host = b.host; 879 880 t.source = ''; 881 } 882 883 //t.path = t.path || '/'; 884 }, 885 886 setPath : function(p) { 887 var t = this; 888 889 p = /^(.*?)\/?(\w+)?$/.exec(p); 890 891 // Update path parts 892 t.path = p[0]; 893 t.directory = p[1]; 894 t.file = p[2]; 895 896 // Rebuild source 897 t.source = ''; 898 t.getURI(); 899 }, 900 901 toRelative : function(u) { 902 var t = this, o; 903 904 if (u === "./") 905 return u; 906 907 u = new tinymce.util.URI(u, {base_uri : t}); 908 909 // Not on same domain/port or protocol 910 if ((u.host != 'mce_host' && t.host != u.host && u.host) || t.port != u.port || t.protocol != u.protocol) 911 return u.getURI(); 912 913 var tu = t.getURI(), uu = u.getURI(); 914 915 // Allow usage of the base_uri when relative_urls = true 916 if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) 917 return tu; 918 919 o = t.toRelPath(t.path, u.path); 920 921 // Add query 922 if (u.query) 923 o += '?' + u.query; 924 925 // Add anchor 926 if (u.anchor) 927 o += '#' + u.anchor; 928 929 return o; 930 }, 931 932 toAbsolute : function(u, nh) { 933 u = new tinymce.util.URI(u, {base_uri : this}); 934 935 return u.getURI(this.host == u.host && this.protocol == u.protocol ? nh : 0); 936 }, 937 938 toRelPath : function(base, path) { 939 var items, bp = 0, out = '', i, l; 940 941 // Split the paths 942 base = base.substring(0, base.lastIndexOf('/')); 943 base = base.split('/'); 944 items = path.split('/'); 945 946 if (base.length >= items.length) { 947 for (i = 0, l = base.length; i < l; i++) { 948 if (i >= items.length || base[i] != items[i]) { 949 bp = i + 1; 950 break; 951 } 952 } 953 } 954 955 if (base.length < items.length) { 956 for (i = 0, l = items.length; i < l; i++) { 957 if (i >= base.length || base[i] != items[i]) { 958 bp = i + 1; 959 break; 960 } 961 } 962 } 963 964 if (bp === 1) 965 return path; 966 967 for (i = 0, l = base.length - (bp - 1); i < l; i++) 968 out += "../"; 969 970 for (i = bp - 1, l = items.length; i < l; i++) { 971 if (i != bp - 1) 972 out += "/" + items[i]; 973 else 974 out += items[i]; 975 } 976 977 return out; 978 }, 979 980 toAbsPath : function(base, path) { 981 var i, nb = 0, o = [], tr, outPath; 982 983 // Split paths 984 tr = /\/$/.test(path) ? '/' : ''; 985 base = base.split('/'); 986 path = path.split('/'); 987 988 // Remove empty chunks 989 each(base, function(k) { 990 if (k) 991 o.push(k); 992 }); 993 994 base = o; 995 996 // Merge relURLParts chunks 997 for (i = path.length - 1, o = []; i >= 0; i--) { 998 // Ignore empty or . 999 if (path[i].length === 0 || path[i] === ".") 1000 continue; 1001 1002 // Is parent 1003 if (path[i] === '..') { 1004 nb++; 1005 continue; 1006 } 1007 1008 // Move up 1009 if (nb > 0) { 1010 nb--; 1011 continue; 1012 } 1013 1014 o.push(path[i]); 1015 } 1016 1017 i = base.length - nb; 1018 1019 // If /a/b/c or / 1020 if (i <= 0) 1021 outPath = o.reverse().join('/'); 1022 else 1023 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 1024 1025 // Add front / if it's needed 1026 if (outPath.indexOf('/') !== 0) 1027 outPath = '/' + outPath; 1028 1029 // Add traling / if it's needed 1030 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) 1031 outPath += tr; 1032 1033 return outPath; 1034 }, 1035 1036 getURI : function(nh) { 1037 var s, t = this; 1038 1039 // Rebuild source 1040 if (!t.source || nh) { 1041 s = ''; 1042 1043 if (!nh) { 1044 if (t.protocol) 1045 s += t.protocol + '://'; 1046 1047 if (t.userInfo) 1048 s += t.userInfo + '@'; 1049 1050 if (t.host) 1051 s += t.host; 1052 1053 if (t.port) 1054 s += ':' + t.port; 1055 } 1056 1057 if (t.path) 1058 s += t.path; 1059 1060 if (t.query) 1061 s += '?' + t.query; 1062 1063 if (t.anchor) 1064 s += '#' + t.anchor; 1065 1066 t.source = s; 1067 } 1068 1069 return t.source; 1070 } 1071 }); 1072 })(); 1073 1074 (function() { 1075 var each = tinymce.each; 1076 1077 tinymce.create('static tinymce.util.Cookie', { 1078 getHash : function(n) { 1079 var v = this.get(n), h; 1080 1081 if (v) { 1082 each(v.split('&'), function(v) { 1083 v = v.split('='); 1084 h = h || {}; 1085 h[unescape(v[0])] = unescape(v[1]); 1086 }); 1087 } 1088 1089 return h; 1090 }, 1091 1092 setHash : function(n, v, e, p, d, s) { 1093 var o = ''; 1094 1095 each(v, function(v, k) { 1096 o += (!o ? '' : '&') + escape(k) + '=' + escape(v); 1097 }); 1098 1099 this.set(n, o, e, p, d, s); 1100 }, 1101 1102 get : function(n) { 1103 var c = document.cookie, e, p = n + "=", b; 1104 1105 // Strict mode 1106 if (!c) 1107 return; 1108 1109 b = c.indexOf("; " + p); 1110 1111 if (b == -1) { 1112 b = c.indexOf(p); 1113 1114 if (b !== 0) 1115 return null; 1116 } else 1117 b += 2; 1118 1119 e = c.indexOf(";", b); 1120 1121 if (e == -1) 1122 e = c.length; 1123 1124 return unescape(c.substring(b + p.length, e)); 1125 }, 1126 1127 set : function(n, v, e, p, d, s) { 1128 document.cookie = n + "=" + escape(v) + 1129 ((e) ? "; expires=" + e.toGMTString() : "") + 1130 ((p) ? "; path=" + escape(p) : "") + 1131 ((d) ? "; domain=" + d : "") + 1132 ((s) ? "; secure" : ""); 1133 }, 1134 1135 remove : function(name, path, domain) { 1136 var date = new Date(); 1137 1138 date.setTime(date.getTime() - 1000); 1139 1140 this.set(name, '', date, path, domain); 1141 } 1142 }); 1143 })(); 1144 1145 (function() { 1146 function serialize(o, quote) { 1147 var i, v, t, name; 1148 1149 quote = quote || '"'; 1150 1151 if (o == null) 1152 return 'null'; 1153 1154 t = typeof o; 1155 1156 if (t == 'string') { 1157 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 1158 1159 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 1160 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 1161 if (quote === '"' && a === "'") 1162 return a; 1163 1164 i = v.indexOf(b); 1165 1166 if (i + 1) 1167 return '\\' + v.charAt(i + 1); 1168 1169 a = b.charCodeAt().toString(16); 1170 1171 return '\\u' + '0000'.substring(a.length) + a; 1172 }) + quote; 1173 } 1174 1175 if (t == 'object') { 1176 if (o.hasOwnProperty && o instanceof Array) { 1177 for (i=0, v = '['; i<o.length; i++) 1178 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 1179 1180 return v + ']'; 1181 } 1182 1183 v = '{'; 1184 1185 for (name in o) { 1186 if (o.hasOwnProperty(name)) { 1187 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + quote +':' + serialize(o[name], quote) : ''; 1188 } 1189 } 1190 1191 return v + '}'; 1192 } 1193 1194 return '' + o; 1195 }; 1196 1197 tinymce.util.JSON = { 1198 serialize: serialize, 1199 1200 parse: function(s) { 1201 try { 1202 return eval('(' + s + ')'); 1203 } catch (ex) { 1204 // Ignore 1205 } 1206 } 1207 1208 }; 1209 })(); 1210 1211 tinymce.create('static tinymce.util.XHR', { 1212 send : function(o) { 1213 var x, t, w = window, c = 0; 1214 1215 function ready() { 1216 if (!o.async || x.readyState == 4 || c++ > 10000) { 1217 if (o.success && c < 10000 && x.status == 200) 1218 o.success.call(o.success_scope, '' + x.responseText, x, o); 1219 else if (o.error) 1220 o.error.call(o.error_scope, c > 10000 ? 'TIMED_OUT' : 'GENERAL', x, o); 1221 1222 x = null; 1223 } else 1224 w.setTimeout(ready, 10); 1225 }; 1226 1227 // Default settings 1228 o.scope = o.scope || this; 1229 o.success_scope = o.success_scope || o.scope; 1230 o.error_scope = o.error_scope || o.scope; 1231 o.async = o.async === false ? false : true; 1232 o.data = o.data || ''; 1233 1234 function get(s) { 1235 x = 0; 1236 1237 try { 1238 x = new ActiveXObject(s); 1239 } catch (ex) { 1240 } 1241 1242 return x; 1243 }; 1244 1245 x = w.XMLHttpRequest ? new XMLHttpRequest() : get('Microsoft.XMLHTTP') || get('Msxml2.XMLHTTP'); 1246 1247 if (x) { 1248 if (x.overrideMimeType) 1249 x.overrideMimeType(o.content_type); 1250 1251 x.open(o.type || (o.data ? 'POST' : 'GET'), o.url, o.async); 1252 1253 if (o.content_type) 1254 x.setRequestHeader('Content-Type', o.content_type); 1255 1256 x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 1257 1258 x.send(o.data); 1259 1260 // Syncronous request 1261 if (!o.async) 1262 return ready(); 1263 1264 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 1265 t = w.setTimeout(ready, 10); 1266 } 1267 } 1268 }); 1269 1270 (function() { 1271 var extend = tinymce.extend, JSON = tinymce.util.JSON, XHR = tinymce.util.XHR; 1272 1273 tinymce.create('tinymce.util.JSONRequest', { 1274 JSONRequest : function(s) { 1275 this.settings = extend({ 1276 }, s); 1277 this.count = 0; 1278 }, 1279 1280 send : function(o) { 1281 var ecb = o.error, scb = o.success; 1282 1283 o = extend(this.settings, o); 1284 1285 o.success = function(c, x) { 1286 c = JSON.parse(c); 1287 1288 if (typeof(c) == 'undefined') { 1289 c = { 1290 error : 'JSON Parse error.' 1291 }; 1292 } 1293 1294 if (c.error) 1295 ecb.call(o.error_scope || o.scope, c.error, x); 1296 else 1297 scb.call(o.success_scope || o.scope, c.result); 1298 }; 1299 1300 o.error = function(ty, x) { 1301 if (ecb) 1302 ecb.call(o.error_scope || o.scope, ty, x); 1303 }; 1304 1305 o.data = JSON.serialize({ 1306 id : o.id || 'c' + (this.count++), 1307 method : o.method, 1308 params : o.params 1309 }); 1310 1311 // JSON content type for Ruby on rails. Bug: #1883287 1312 o.content_type = 'application/json'; 1313 1314 XHR.send(o); 1315 }, 1316 1317 'static' : { 1318 sendRPC : function(o) { 1319 return new tinymce.util.JSONRequest().send(o); 1320 } 1321 } 1322 }); 1323 }()); 1324 (function(tinymce){ 1325 tinymce.VK = { 1326 BACKSPACE: 8, 1327 DELETE: 46, 1328 DOWN: 40, 1329 ENTER: 13, 1330 LEFT: 37, 1331 RIGHT: 39, 1332 SPACEBAR: 32, 1333 TAB: 9, 1334 UP: 38, 1335 1336 modifierPressed: function (e) { 1337 return e.shiftKey || e.ctrlKey || e.altKey; 1338 }, 1339 1340 metaKeyPressed: function(e) { 1341 return tinymce.isMac ? e.metaKey : e.ctrlKey; 1342 } 1343 }; 1344 })(tinymce); 1345 1346 tinymce.util.Quirks = function(editor) { 1347 var VK = tinymce.VK, BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, settings = editor.settings; 1348 1349 function setEditorCommandState(cmd, state) { 1350 try { 1351 editor.getDoc().execCommand(cmd, false, state); 1352 } catch (ex) { 1353 // Ignore 1354 } 1355 } 1356 1357 function getDocumentMode() { 1358 var documentMode = editor.getDoc().documentMode; 1359 1360 return documentMode ? documentMode : 6; 1361 }; 1362 1363 function cleanupStylesWhenDeleting() { 1364 function removeMergedFormatSpans(isDelete) { 1365 var rng, blockElm, node, clonedSpan; 1366 1367 rng = selection.getRng(); 1368 1369 // Find root block 1370 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1371 1372 // On delete clone the root span of the next block element 1373 if (isDelete) 1374 blockElm = dom.getNext(blockElm, dom.isBlock); 1375 1376 // Locate root span element and clone it since it would otherwise get merged by the "apple-style-span" on delete/backspace 1377 if (blockElm) { 1378 node = blockElm.firstChild; 1379 1380 // Ignore empty text nodes 1381 while (node && node.nodeType == 3 && node.nodeValue.length === 0) 1382 node = node.nextSibling; 1383 1384 if (node && node.nodeName === 'SPAN') { 1385 clonedSpan = node.cloneNode(false); 1386 } 1387 } 1388 1389 // Do the backspace/delete action 1390 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1391 1392 // Find all odd apple-style-spans 1393 blockElm = dom.getParent(rng.startContainer, dom.isBlock); 1394 tinymce.each(dom.select('span.Apple-style-span,font.Apple-style-span', blockElm), function(span) { 1395 var bm = selection.getBookmark(); 1396 1397 if (clonedSpan) { 1398 dom.replace(clonedSpan.cloneNode(false), span, true); 1399 } else { 1400 dom.remove(span, true); 1401 } 1402 1403 // Restore the selection 1404 selection.moveToBookmark(bm); 1405 }); 1406 }; 1407 1408 editor.onKeyDown.add(function(editor, e) { 1409 var isDelete; 1410 1411 isDelete = e.keyCode == DELETE; 1412 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1413 e.preventDefault(); 1414 removeMergedFormatSpans(isDelete); 1415 } 1416 }); 1417 1418 editor.addCommand('Delete', function() {removeMergedFormatSpans();}); 1419 }; 1420 1421 function emptyEditorWhenDeleting() { 1422 function serializeRng(rng) { 1423 var body = dom.create("body"); 1424 var contents = rng.cloneContents(); 1425 body.appendChild(contents); 1426 return selection.serializer.serialize(body, {format: 'html'}); 1427 } 1428 1429 function allContentsSelected(rng) { 1430 var selection = serializeRng(rng); 1431 1432 var allRng = dom.createRng(); 1433 allRng.selectNode(editor.getBody()); 1434 1435 var allSelection = serializeRng(allRng);//console.log(selection, "----", allSelection); 1436 return selection === allSelection; 1437 } 1438 1439 editor.onKeyDown.add(function(editor, e) { 1440 var keyCode = e.keyCode, isCollapsed; 1441 1442 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 1443 if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) { 1444 isCollapsed = editor.selection.isCollapsed(); 1445 1446 // Selection is collapsed but the editor isn't empty 1447 if (isCollapsed && !dom.isEmpty(editor.getBody())) { 1448 return; 1449 } 1450 1451 // IE deletes all contents correctly when everything is selected 1452 if (tinymce.isIE && !isCollapsed) { 1453 return; 1454 } 1455 1456 // Selection isn't collapsed but not all the contents is selected 1457 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 1458 return; 1459 } 1460 1461 // Manually empty the editor 1462 editor.setContent(''); 1463 editor.selection.setCursorLocation(editor.getBody(), 0); 1464 editor.nodeChanged(); 1465 } 1466 }); 1467 }; 1468 1469 function selectAll() { 1470 editor.onKeyDown.add(function(editor, e) { 1471 if (e.keyCode == 65 && VK.metaKeyPressed(e)) { 1472 e.preventDefault(); 1473 editor.execCommand('SelectAll'); 1474 } 1475 }); 1476 }; 1477 1478 function inputMethodFocus() { 1479 if (!editor.settings.content_editable) { 1480 // Case 1 IME doesn't initialize if you focus the document 1481 dom.bind(editor.getDoc(), 'focusin', function(e) { 1482 selection.setRng(selection.getRng()); 1483 }); 1484 1485 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 1486 dom.bind(editor.getDoc(), 'mousedown', function(e) { 1487 if (e.target == editor.getDoc().documentElement) { 1488 editor.getWin().focus(); 1489 selection.setRng(selection.getRng()); 1490 } 1491 }); 1492 } 1493 }; 1494 1495 function removeHrOnBackspace() { 1496 editor.onKeyDown.add(function(editor, e) { 1497 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1498 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1499 var node = selection.getNode(); 1500 var previousSibling = node.previousSibling; 1501 1502 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 1503 dom.remove(previousSibling); 1504 tinymce.dom.Event.cancel(e); 1505 } 1506 } 1507 } 1508 }) 1509 } 1510 1511 function focusBody() { 1512 // Fix for a focus bug in FF 3.x where the body element 1513 // wouldn't get proper focus if the user clicked on the HTML element 1514 if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 1515 editor.onMouseDown.add(function(editor, e) { 1516 if (e.target.nodeName === "HTML") { 1517 var body = editor.getBody(); 1518 1519 // Blur the body it's focused but not correctly focused 1520 body.blur(); 1521 1522 // Refocus the body after a little while 1523 setTimeout(function() { 1524 body.focus(); 1525 }, 0); 1526 } 1527 }); 1528 } 1529 }; 1530 1531 function selectControlElements() { 1532 editor.onClick.add(function(editor, e) { 1533 e = e.target; 1534 1535 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 1536 // WebKit can't even do simple things like selecting an image 1537 // Needs tobe the setBaseAndExtend or it will fail to select floated images 1538 if (/^(IMG|HR)$/.test(e.nodeName)) { 1539 selection.getSel().setBaseAndExtent(e, 0, e, 1); 1540 } 1541 1542 if (e.nodeName == 'A' && dom.hasClass(e, 'mceItemAnchor')) { 1543 selection.select(e); 1544 } 1545 1546 editor.nodeChanged(); 1547 }); 1548 }; 1549 1550 function removeStylesWhenDeletingAccrossBlockElements() { 1551 function getAttributeApplyFunction() { 1552 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 1553 1554 return function() { 1555 var target = selection.getStart(); 1556 1557 if (target !== editor.getBody()) { 1558 dom.setAttrib(target, "style", null); 1559 1560 tinymce.each(template, function(attr) { 1561 target.setAttributeNode(attr.cloneNode(true)); 1562 }); 1563 } 1564 }; 1565 } 1566 1567 function isSelectionAcrossElements() { 1568 return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); 1569 } 1570 1571 function blockEvent(editor, e) { 1572 e.preventDefault(); 1573 return false; 1574 } 1575 1576 editor.onKeyPress.add(function(editor, e) { 1577 var applyAttributes; 1578 1579 if ((e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 1580 applyAttributes = getAttributeApplyFunction(); 1581 editor.getDoc().execCommand('delete', false, null); 1582 applyAttributes(); 1583 e.preventDefault(); 1584 return false; 1585 } 1586 }); 1587 1588 dom.bind(editor.getDoc(), 'cut', function(e) { 1589 var applyAttributes; 1590 1591 if (isSelectionAcrossElements()) { 1592 applyAttributes = getAttributeApplyFunction(); 1593 editor.onKeyUp.addToTop(blockEvent); 1594 1595 setTimeout(function() { 1596 applyAttributes(); 1597 editor.onKeyUp.remove(blockEvent); 1598 }, 0); 1599 } 1600 }); 1601 } 1602 1603 function selectionChangeNodeChanged() { 1604 var lastRng, selectionTimer; 1605 1606 dom.bind(editor.getDoc(), 'selectionchange', function() { 1607 if (selectionTimer) { 1608 clearTimeout(selectionTimer); 1609 selectionTimer = 0; 1610 } 1611 1612 selectionTimer = window.setTimeout(function() { 1613 var rng = selection.getRng(); 1614 1615 // Compare the ranges to see if it was a real change or not 1616 if (!lastRng || !tinymce.dom.RangeUtils.compareRanges(rng, lastRng)) { 1617 editor.nodeChanged(); 1618 lastRng = rng; 1619 } 1620 }, 50); 1621 }); 1622 } 1623 1624 function ensureBodyHasRoleApplication() { 1625 document.body.setAttribute("role", "application"); 1626 } 1627 1628 function disableBackspaceIntoATable() { 1629 editor.onKeyDown.add(function(editor, e) { 1630 if (!e.isDefaultPrevented() && e.keyCode === BACKSPACE) { 1631 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 1632 var previousSibling = selection.getNode().previousSibling; 1633 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 1634 return tinymce.dom.Event.cancel(e); 1635 } 1636 } 1637 } 1638 }) 1639 } 1640 1641 function addNewLinesBeforeBrInPre() { 1642 // IE8+ rendering mode does the right thing with BR in PRE 1643 if (getDocumentMode() > 7) { 1644 return; 1645 } 1646 1647 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 1648 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 1649 setEditorCommandState('RespectVisibilityInDesign', true); 1650 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 1651 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 1652 1653 // Adds a \n before all BR elements in PRE to get them visual 1654 editor.parser.addNodeFilter('pre', function(nodes, name) { 1655 var i = nodes.length, brNodes, j, brElm, sibling; 1656 1657 while (i--) { 1658 brNodes = nodes[i].getAll('br'); 1659 j = brNodes.length; 1660 while (j--) { 1661 brElm = brNodes[j]; 1662 1663 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 1664 sibling = brElm.prev; 1665 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 1666 sibling.value += '\n'; 1667 } else { 1668 brElm.parent.insert(new tinymce.html.Node('#text', 3), brElm, true).value = '\n'; 1669 } 1670 } 1671 } 1672 }); 1673 1674 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 1675 editor.serializer.addNodeFilter('pre', function(nodes, name) { 1676 var i = nodes.length, brNodes, j, brElm, sibling; 1677 1678 while (i--) { 1679 brNodes = nodes[i].getAll('br'); 1680 j = brNodes.length; 1681 while (j--) { 1682 brElm = brNodes[j]; 1683 sibling = brElm.prev; 1684 if (sibling && sibling.type == 3) { 1685 sibling.value = sibling.value.replace(/\r?\n$/, ''); 1686 } 1687 } 1688 } 1689 }); 1690 } 1691 1692 function removePreSerializedStylesWhenSelectingControls() { 1693 dom.bind(editor.getBody(), 'mouseup', function(e) { 1694 var value, node = selection.getNode(); 1695 1696 // Moved styles to attributes on IMG eements 1697 if (node.nodeName == 'IMG') { 1698 // Convert style width to width attribute 1699 if (value = dom.getStyle(node, 'width')) { 1700 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 1701 dom.setStyle(node, 'width', ''); 1702 } 1703 1704 // Convert style height to height attribute 1705 if (value = dom.getStyle(node, 'height')) { 1706 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 1707 dom.setStyle(node, 'height', ''); 1708 } 1709 } 1710 }); 1711 } 1712 1713 function keepInlineElementOnDeleteBackspace() { 1714 editor.onKeyDown.add(function(editor, e) { 1715 var isDelete, rng, container, offset, brElm, sibling, collapsed; 1716 1717 isDelete = e.keyCode == DELETE; 1718 if (!e.isDefaultPrevented() && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) { 1719 rng = selection.getRng(); 1720 container = rng.startContainer; 1721 offset = rng.startOffset; 1722 collapsed = rng.collapsed; 1723 1724 // Override delete if the start container is a text node and is at the beginning of text or 1725 // just before/after the last character to be deleted in collapsed mode 1726 if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) || (collapsed && offset === (isDelete ? 0 : 1)))) { 1727 nonEmptyElements = editor.schema.getNonEmptyElements(); 1728 1729 // Prevent default logic since it's broken 1730 e.preventDefault(); 1731 1732 // Insert a BR before the text node this will prevent the containing element from being deleted/converted 1733 brElm = dom.create('br', {id: '__tmp'}); 1734 container.parentNode.insertBefore(brElm, container); 1735 1736 // Do the browser delete 1737 editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null); 1738 1739 // Check if the previous sibling is empty after deleting for example: <p><b></b>|</p> 1740 container = selection.getRng().startContainer; 1741 sibling = container.previousSibling; 1742 if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) && !nonEmptyElements[sibling.nodeName.toLowerCase()]) { 1743 dom.remove(sibling); 1744 } 1745 1746 // Remove the temp element we inserted 1747 dom.remove('__tmp'); 1748 } 1749 } 1750 }); 1751 } 1752 1753 function removeBlockQuoteOnBackSpace() { 1754 // Add block quote deletion handler 1755 editor.onKeyDown.add(function(editor, e) { 1756 var rng, container, offset, root, parent; 1757 1758 if (e.isDefaultPrevented() || e.keyCode != VK.BACKSPACE) { 1759 return; 1760 } 1761 1762 rng = selection.getRng(); 1763 container = rng.startContainer; 1764 offset = rng.startOffset; 1765 root = dom.getRoot(); 1766 parent = container; 1767 1768 if (!rng.collapsed || offset !== 0) { 1769 return; 1770 } 1771 1772 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 1773 parent = parent.parentNode; 1774 } 1775 1776 // Is the cursor at the beginning of a blockquote? 1777 if (parent.tagName === 'BLOCKQUOTE') { 1778 // Remove the blockquote 1779 editor.formatter.toggle('blockquote', null, parent); 1780 1781 // Move the caret to the beginning of container 1782 rng.setStart(container, 0); 1783 rng.setEnd(container, 0); 1784 selection.setRng(rng); 1785 selection.collapse(false); 1786 } 1787 }); 1788 }; 1789 1790 function setGeckoEditingOptions() { 1791 function setOpts() { 1792 editor._refreshContentEditable(); 1793 1794 setEditorCommandState("StyleWithCSS", false); 1795 setEditorCommandState("enableInlineTableEditing", false); 1796 1797 if (!settings.object_resizing) { 1798 setEditorCommandState("enableObjectResizing", false); 1799 } 1800 }; 1801 1802 if (!settings.readonly) { 1803 editor.onBeforeExecCommand.add(setOpts); 1804 editor.onMouseDown.add(setOpts); 1805 } 1806 }; 1807 1808 function addBrAfterLastLinks() { 1809 function fixLinks(editor, o) { 1810 tinymce.each(dom.select('a'), function(node) { 1811 var parentNode = node.parentNode, root = dom.getRoot(); 1812 1813 if (parentNode.lastChild === node) { 1814 while (parentNode && !dom.isBlock(parentNode)) { 1815 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 1816 return; 1817 } 1818 1819 parentNode = parentNode.parentNode; 1820 } 1821 1822 dom.add(parentNode, 'br', {'data-mce-bogus' : 1}); 1823 } 1824 }); 1825 }; 1826 1827 editor.onExecCommand.add(function(editor, cmd) { 1828 if (cmd === 'CreateLink') { 1829 fixLinks(editor); 1830 } 1831 }); 1832 1833 editor.onSetContent.add(selection.onSetContent.add(fixLinks)); 1834 }; 1835 1836 function setDefaultBlockType() { 1837 if (settings.forced_root_block) { 1838 editor.onInit.add(function() { 1839 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 1840 }); 1841 } 1842 } 1843 1844 function removeGhostSelection() { 1845 function repaint(sender, args) { 1846 if (!sender || !args.initial) { 1847 editor.execCommand('mceRepaint'); 1848 } 1849 }; 1850 1851 editor.onUndo.add(repaint); 1852 editor.onRedo.add(repaint); 1853 editor.onSetContent.add(repaint); 1854 }; 1855 1856 function deleteControlItemOnBackSpace() { 1857 editor.onKeyDown.add(function(editor, e) { 1858 var rng; 1859 1860 if (!e.isDefaultPrevented() && e.keyCode == BACKSPACE) { 1861 rng = editor.getDoc().selection.createRange(); 1862 if (rng && rng.item) { 1863 e.preventDefault(); 1864 editor.undoManager.beforeChange(); 1865 dom.remove(rng.item(0)); 1866 editor.undoManager.add(); 1867 } 1868 } 1869 }); 1870 }; 1871 1872 function renderEmptyBlocksFix() { 1873 var emptyBlocksCSS; 1874 1875 // IE10+ 1876 if (getDocumentMode() >= 10) { 1877 emptyBlocksCSS = ''; 1878 tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 1879 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 1880 }); 1881 1882 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 1883 } 1884 }; 1885 1886 function fakeImageResize() { 1887 var mouseDownImg, startX, startY, startW, startH; 1888 1889 if (!settings.object_resizing || settings.webkit_fake_resize === false) { 1890 return; 1891 } 1892 1893 editor.contentStyles.push('.mceResizeImages img {cursor: se-resize !important}'); 1894 1895 function resizeImage(e) { 1896 var deltaX, deltaY, ratio, width, height; 1897 1898 if (mouseDownImg) { 1899 deltaX = e.screenX - startX; 1900 deltaY = e.screenY - startY; 1901 ratio = Math.max((startW + deltaX) / startW, (startH + deltaY) / startH); 1902 1903 // Only update styles if the user draged one pixel or more 1904 if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { 1905 // Constrain proportions 1906 width = Math.round(startW * ratio); 1907 height = Math.round(startH * ratio); 1908 1909 // Resize by using style or attribute 1910 if (mouseDownImg.style.width) { 1911 dom.setStyle(mouseDownImg, 'width', width); 1912 } else { 1913 dom.setAttrib(mouseDownImg, 'width', width); 1914 } 1915 1916 // Resize by using style or attribute 1917 if (mouseDownImg.style.height) { 1918 dom.setStyle(mouseDownImg, 'height', height); 1919 } else { 1920 dom.setAttrib(mouseDownImg, 'height', height); 1921 } 1922 1923 if (!dom.hasClass(editor.getBody(), 'mceResizeImages')) { 1924 dom.addClass(editor.getBody(), 'mceResizeImages'); 1925 } 1926 } 1927 } 1928 }; 1929 1930 editor.onMouseDown.add(function(editor, e) { 1931 var target = e.target; 1932 1933 if (target.nodeName == "IMG") { 1934 mouseDownImg = target; 1935 startX = e.screenX; 1936 startY = e.screenY; 1937 startW = mouseDownImg.clientWidth; 1938 startH = mouseDownImg.clientHeight; 1939 dom.bind(editor.getDoc(), 'mousemove', resizeImage); 1940 e.preventDefault(); 1941 } 1942 }); 1943 1944 // Unbind events on node change and restore resize cursor 1945 editor.onNodeChange.add(function() { 1946 if (mouseDownImg) { 1947 mouseDownImg = null; 1948 dom.unbind(editor.getDoc(), 'mousemove', resizeImage); 1949 } 1950 1951 if (selection.getNode().nodeName == "IMG") { 1952 dom.addClass(editor.getBody(), 'mceResizeImages'); 1953 } else { 1954 dom.removeClass(editor.getBody(), 'mceResizeImages'); 1955 } 1956 }); 1957 }; 1958 1959 // All browsers 1960 disableBackspaceIntoATable(); 1961 removeBlockQuoteOnBackSpace(); 1962 emptyEditorWhenDeleting(); 1963 1964 // WebKit 1965 if (tinymce.isWebKit) { 1966 keepInlineElementOnDeleteBackspace(); 1967 cleanupStylesWhenDeleting(); 1968 inputMethodFocus(); 1969 selectControlElements(); 1970 setDefaultBlockType(); 1971 1972 // iOS 1973 if (tinymce.isIDevice) { 1974 selectionChangeNodeChanged(); 1975 } else { 1976 fakeImageResize(); 1977 selectAll(); 1978 } 1979 } 1980 1981 // IE 1982 if (tinymce.isIE) { 1983 removeHrOnBackspace(); 1984 ensureBodyHasRoleApplication(); 1985 addNewLinesBeforeBrInPre(); 1986 removePreSerializedStylesWhenSelectingControls(); 1987 deleteControlItemOnBackSpace(); 1988 renderEmptyBlocksFix(); 1989 } 1990 1991 // Gecko 1992 if (tinymce.isGecko) { 1993 removeHrOnBackspace(); 1994 focusBody(); 1995 removeStylesWhenDeletingAccrossBlockElements(); 1996 setGeckoEditingOptions(); 1997 addBrAfterLastLinks(); 1998 removeGhostSelection(); 1999 } 2000 }; 2001 (function(tinymce) { 2002 var namedEntities, baseEntities, reverseEntities, 2003 attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2004 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 2005 rawCharsRegExp = /[<>&\"\']/g, 2006 entityRegExp = /&(#x|#)?([\w]+);/g, 2007 asciiMap = { 2008 128 : "\u20AC", 130 : "\u201A", 131 : "\u0192", 132 : "\u201E", 133 : "\u2026", 134 : "\u2020", 2009 135 : "\u2021", 136 : "\u02C6", 137 : "\u2030", 138 : "\u0160", 139 : "\u2039", 140 : "\u0152", 2010 142 : "\u017D", 145 : "\u2018", 146 : "\u2019", 147 : "\u201C", 148 : "\u201D", 149 : "\u2022", 2011 150 : "\u2013", 151 : "\u2014", 152 : "\u02DC", 153 : "\u2122", 154 : "\u0161", 155 : "\u203A", 2012 156 : "\u0153", 158 : "\u017E", 159 : "\u0178" 2013 }; 2014 2015 // Raw entities 2016 baseEntities = { 2017 '\"' : '"', // Needs to be escaped since the YUI compressor would otherwise break the code 2018 "'" : ''', 2019 '<' : '<', 2020 '>' : '>', 2021 '&' : '&' 2022 }; 2023 2024 // Reverse lookup table for raw entities 2025 reverseEntities = { 2026 '<' : '<', 2027 '>' : '>', 2028 '&' : '&', 2029 '"' : '"', 2030 ''' : "'" 2031 }; 2032 2033 // Decodes text by using the browser 2034 function nativeDecode(text) { 2035 var elm; 2036 2037 elm = document.createElement("div"); 2038 elm.innerHTML = text; 2039 2040 return elm.textContent || elm.innerText || text; 2041 }; 2042 2043 // Build a two way lookup table for the entities 2044 function buildEntitiesLookup(items, radix) { 2045 var i, chr, entity, lookup = {}; 2046 2047 if (items) { 2048 items = items.split(','); 2049 radix = radix || 10; 2050 2051 // Build entities lookup table 2052 for (i = 0; i < items.length; i += 2) { 2053 chr = String.fromCharCode(parseInt(items[i], radix)); 2054 2055 // Only add non base entities 2056 if (!baseEntities[chr]) { 2057 entity = '&' + items[i + 1] + ';'; 2058 lookup[chr] = entity; 2059 lookup[entity] = chr; 2060 } 2061 } 2062 2063 return lookup; 2064 } 2065 }; 2066 2067 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 2068 namedEntities = buildEntitiesLookup( 2069 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 2070 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 2071 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 2072 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 2073 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 2074 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 2075 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 2076 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 2077 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 2078 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 2079 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 2080 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 2081 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 2082 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 2083 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 2084 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 2085 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 2086 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 2087 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 2088 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 2089 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 2090 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 2091 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 2092 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 2093 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 2094 2095 tinymce.html = tinymce.html || {}; 2096 2097 tinymce.html.Entities = { 2098 encodeRaw : function(text, attr) { 2099 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2100 return baseEntities[chr] || chr; 2101 }); 2102 }, 2103 2104 encodeAllRaw : function(text) { 2105 return ('' + text).replace(rawCharsRegExp, function(chr) { 2106 return baseEntities[chr] || chr; 2107 }); 2108 }, 2109 2110 encodeNumeric : function(text, attr) { 2111 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2112 // Multi byte sequence convert it to a single entity 2113 if (chr.length > 1) 2114 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 2115 2116 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 2117 }); 2118 }, 2119 2120 encodeNamed : function(text, attr, entities) { 2121 entities = entities || namedEntities; 2122 2123 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2124 return baseEntities[chr] || entities[chr] || chr; 2125 }); 2126 }, 2127 2128 getEncodeFunc : function(name, entities) { 2129 var Entities = tinymce.html.Entities; 2130 2131 entities = buildEntitiesLookup(entities) || namedEntities; 2132 2133 function encodeNamedAndNumeric(text, attr) { 2134 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 2135 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 2136 }); 2137 }; 2138 2139 function encodeCustomNamed(text, attr) { 2140 return Entities.encodeNamed(text, attr, entities); 2141 }; 2142 2143 // Replace + with , to be compatible with previous TinyMCE versions 2144 name = tinymce.makeMap(name.replace(/\+/g, ',')); 2145 2146 // Named and numeric encoder 2147 if (name.named && name.numeric) 2148 return encodeNamedAndNumeric; 2149 2150 // Named encoder 2151 if (name.named) { 2152 // Custom names 2153 if (entities) 2154 return encodeCustomNamed; 2155 2156 return Entities.encodeNamed; 2157 } 2158 2159 // Numeric 2160 if (name.numeric) 2161 return Entities.encodeNumeric; 2162 2163 // Raw encoder 2164 return Entities.encodeRaw; 2165 }, 2166 2167 decode : function(text) { 2168 return text.replace(entityRegExp, function(all, numeric, value) { 2169 if (numeric) { 2170 value = parseInt(value, numeric.length === 2 ? 16 : 10); 2171 2172 // Support upper UTF 2173 if (value > 0xFFFF) { 2174 value -= 0x10000; 2175 2176 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 2177 } else 2178 return asciiMap[value] || String.fromCharCode(value); 2179 } 2180 2181 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 2182 }); 2183 } 2184 }; 2185 })(tinymce); 2186 2187 tinymce.html.Styles = function(settings, schema) { 2188 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 2189 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 2190 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 2191 trimRightRegExp = /\s+$/, 2192 urlColorRegExp = /rgb/, 2193 undef, i, encodingLookup = {}, encodingItems; 2194 2195 settings = settings || {}; 2196 2197 encodingItems = '\\" \\\' \\; \\: ; : \uFEFF'.split(' '); 2198 for (i = 0; i < encodingItems.length; i++) { 2199 encodingLookup[encodingItems[i]] = '\uFEFF' + i; 2200 encodingLookup['\uFEFF' + i] = encodingItems[i]; 2201 } 2202 2203 function toHex(match, r, g, b) { 2204 function hex(val) { 2205 val = parseInt(val).toString(16); 2206 2207 return val.length > 1 ? val : '0' + val; // 0 -> 00 2208 }; 2209 2210 return '#' + hex(r) + hex(g) + hex(b); 2211 }; 2212 2213 return { 2214 toHex : function(color) { 2215 return color.replace(rgbRegExp, toHex); 2216 }, 2217 2218 parse : function(css) { 2219 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope || this; 2220 2221 function compress(prefix, suffix) { 2222 var top, right, bottom, left; 2223 2224 // Get values and check it it needs compressing 2225 top = styles[prefix + '-top' + suffix]; 2226 if (!top) 2227 return; 2228 2229 right = styles[prefix + '-right' + suffix]; 2230 if (top != right) 2231 return; 2232 2233 bottom = styles[prefix + '-bottom' + suffix]; 2234 if (right != bottom) 2235 return; 2236 2237 left = styles[prefix + '-left' + suffix]; 2238 if (bottom != left) 2239 return; 2240 2241 // Compress 2242 styles[prefix + suffix] = left; 2243 delete styles[prefix + '-top' + suffix]; 2244 delete styles[prefix + '-right' + suffix]; 2245 delete styles[prefix + '-bottom' + suffix]; 2246 delete styles[prefix + '-left' + suffix]; 2247 }; 2248 2249 function canCompress(key) { 2250 var value = styles[key], i; 2251 2252 if (!value || value.indexOf(' ') < 0) 2253 return; 2254 2255 value = value.split(' '); 2256 i = value.length; 2257 while (i--) { 2258 if (value[i] !== value[0]) 2259 return false; 2260 } 2261 2262 styles[key] = value[0]; 2263 2264 return true; 2265 }; 2266 2267 function compress2(target, a, b, c) { 2268 if (!canCompress(a)) 2269 return; 2270 2271 if (!canCompress(b)) 2272 return; 2273 2274 if (!canCompress(c)) 2275 return; 2276 2277 // Compress 2278 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 2279 delete styles[a]; 2280 delete styles[b]; 2281 delete styles[c]; 2282 }; 2283 2284 // Encodes the specified string by replacing all \" \' ; : with _<num> 2285 function encode(str) { 2286 isEncoded = true; 2287 2288 return encodingLookup[str]; 2289 }; 2290 2291 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 2292 // It will also decode the \" \' if keep_slashes is set to fale or omitted 2293 function decode(str, keep_slashes) { 2294 if (isEncoded) { 2295 str = str.replace(/\uFEFF[0-9]/g, function(str) { 2296 return encodingLookup[str]; 2297 }); 2298 } 2299 2300 if (!keep_slashes) 2301 str = str.replace(/\\([\'\";:])/g, "$1"); 2302 2303 return str; 2304 }; 2305 2306 function processUrl(match, url, url2, url3, str, str2) { 2307 str = str || str2; 2308 2309 if (str) { 2310 str = decode(str); 2311 2312 // Force strings into single quote format 2313 return "'" + str.replace(/\'/g, "\\'") + "'"; 2314 } 2315 2316 url = decode(url || url2 || url3); 2317 2318 // Convert the URL to relative/absolute depending on config 2319 if (urlConverter) 2320 url = urlConverter.call(urlConverterScope, url, 'style'); 2321 2322 // Output new URL format 2323 return "url('" + url.replace(/\'/g, "\\'") + "')"; 2324 }; 2325 2326 if (css) { 2327 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 2328 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 2329 return str.replace(/[;:]/g, encode); 2330 }); 2331 2332 // Parse styles 2333 while (matches = styleRegExp.exec(css)) { 2334 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 2335 value = matches[2].replace(trimRightRegExp, ''); 2336 2337 if (name && value.length > 0) { 2338 // Opera will produce 700 instead of bold in their style values 2339 if (name === 'font-weight' && value === '700') 2340 value = 'bold'; 2341 else if (name === 'color' || name === 'background-color') // Lowercase colors like RED 2342 value = value.toLowerCase(); 2343 2344 // Convert RGB colors to HEX 2345 value = value.replace(rgbRegExp, toHex); 2346 2347 // Convert URLs and force them into url('value') format 2348 value = value.replace(urlOrStrRegExp, processUrl); 2349 styles[name] = isEncoded ? decode(value, true) : value; 2350 } 2351 2352 styleRegExp.lastIndex = matches.index + matches[0].length; 2353 } 2354 2355 // Compress the styles to reduce it's size for example IE will expand styles 2356 compress("border", ""); 2357 compress("border", "-width"); 2358 compress("border", "-color"); 2359 compress("border", "-style"); 2360 compress("padding", ""); 2361 compress("margin", ""); 2362 compress2('border', 'border-width', 'border-style', 'border-color'); 2363 2364 // Remove pointless border, IE produces these 2365 if (styles.border === 'medium none') 2366 delete styles.border; 2367 } 2368 2369 return styles; 2370 }, 2371 2372 serialize : function(styles, element_name) { 2373 var css = '', name, value; 2374 2375 function serializeStyles(name) { 2376 var styleList, i, l, value; 2377 2378 styleList = schema.styles[name]; 2379 if (styleList) { 2380 for (i = 0, l = styleList.length; i < l; i++) { 2381 name = styleList[i]; 2382 value = styles[name]; 2383 2384 if (value !== undef && value.length > 0) 2385 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2386 } 2387 } 2388 }; 2389 2390 // Serialize styles according to schema 2391 if (element_name && schema && schema.styles) { 2392 // Serialize global styles and element specific styles 2393 serializeStyles('*'); 2394 serializeStyles(element_name); 2395 } else { 2396 // Output the styles in the order they are inside the object 2397 for (name in styles) { 2398 value = styles[name]; 2399 2400 if (value !== undef && value.length > 0) 2401 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 2402 } 2403 } 2404 2405 return css; 2406 } 2407 }; 2408 }; 2409 2410 (function(tinymce) { 2411 var mapCache = {}, makeMap = tinymce.makeMap, each = tinymce.each; 2412 2413 function split(str, delim) { 2414 return str.split(delim || ','); 2415 }; 2416 2417 function unpack(lookup, data) { 2418 var key, elements = {}; 2419 2420 function replace(value) { 2421 return value.replace(/[A-Z]+/g, function(key) { 2422 return replace(lookup[key]); 2423 }); 2424 }; 2425 2426 // Unpack lookup 2427 for (key in lookup) { 2428 if (lookup.hasOwnProperty(key)) 2429 lookup[key] = replace(lookup[key]); 2430 } 2431 2432 // Unpack and parse data into object map 2433 replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]\[([^\]]*)\]/g, function(str, name, attributes, children) { 2434 attributes = split(attributes, '|'); 2435 2436 elements[name] = { 2437 attributes : makeMap(attributes), 2438 attributesOrder : attributes, 2439 children : makeMap(children, '|', {'#comment' : {}}) 2440 } 2441 }); 2442 2443 return elements; 2444 }; 2445 2446 function getHTML5() { 2447 var html5 = mapCache.html5; 2448 2449 if (!html5) { 2450 html5 = mapCache.html5 = unpack({ 2451 A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2452 B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' + 2453 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr', 2454 C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' + 2455 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' + 2456 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' 2457 }, 'html[A|manifest][body|head]' + 2458 'head[A][base|command|link|meta|noscript|script|style|title]' + 2459 'title[A][#]' + 2460 'base[A|href|target][]' + 2461 'link[A|href|rel|media|type|sizes][]' + 2462 'meta[A|http-equiv|name|content|charset][]' + 2463 'style[A|type|media|scoped][#]' + 2464 'script[A|charset|type|src|defer|async][#]' + 2465 'noscript[A][C]' + 2466 'body[A][C]' + 2467 'section[A][C]' + 2468 'nav[A][C]' + 2469 'article[A][C]' + 2470 'aside[A][C]' + 2471 'h1[A][B]' + 2472 'h2[A][B]' + 2473 'h3[A][B]' + 2474 'h4[A][B]' + 2475 'h5[A][B]' + 2476 'h6[A][B]' + 2477 'hgroup[A][h1|h2|h3|h4|h5|h6]' + 2478 'header[A][C]' + 2479 'footer[A][C]' + 2480 'address[A][C]' + 2481 'p[A][B]' + 2482 'br[A][]' + 2483 'pre[A][B]' + 2484 'dialog[A][dd|dt]' + 2485 'blockquote[A|cite][C]' + 2486 'ol[A|start|reversed][li]' + 2487 'ul[A][li]' + 2488 'li[A|value][C]' + 2489 'dl[A][dd|dt]' + 2490 'dt[A][B]' + 2491 'dd[A][C]' + 2492 'a[A|href|target|ping|rel|media|type][B]' + 2493 'em[A][B]' + 2494 'strong[A][B]' + 2495 'small[A][B]' + 2496 'cite[A][B]' + 2497 'q[A|cite][B]' + 2498 'dfn[A][B]' + 2499 'abbr[A][B]' + 2500 'code[A][B]' + 2501 'var[A][B]' + 2502 'samp[A][B]' + 2503 'kbd[A][B]' + 2504 'sub[A][B]' + 2505 'sup[A][B]' + 2506 'i[A][B]' + 2507 'b[A][B]' + 2508 'mark[A][B]' + 2509 'progress[A|value|max][B]' + 2510 'meter[A|value|min|max|low|high|optimum][B]' + 2511 'time[A|datetime][B]' + 2512 'ruby[A][B|rt|rp]' + 2513 'rt[A][B]' + 2514 'rp[A][B]' + 2515 'bdo[A][B]' + 2516 'span[A][B]' + 2517 'ins[A|cite|datetime][B]' + 2518 'del[A|cite|datetime][B]' + 2519 'figure[A][C|legend|figcaption]' + 2520 'figcaption[A][C]' + 2521 'img[A|alt|src|height|width|usemap|ismap][]' + 2522 'iframe[A|name|src|height|width|sandbox|seamless][]' + 2523 'embed[A|src|height|width|type][]' + 2524 'object[A|data|type|height|width|usemap|name|form|classid][param]' + 2525 'param[A|name|value][]' + 2526 'details[A|open][C|legend]' + 2527 'command[A|type|label|icon|disabled|checked|radiogroup][]' + 2528 'menu[A|type|label][C|li]' + 2529 'legend[A][C|B]' + 2530 'div[A][C]' + 2531 'source[A|src|type|media][]' + 2532 'audio[A|src|autobuffer|autoplay|loop|controls][source]' + 2533 'video[A|src|autobuffer|autoplay|loop|controls|width|height|poster][source]' + 2534 'hr[A][]' + 2535 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + 2536 'fieldset[A|disabled|form|name][C|legend]' + 2537 'label[A|form|for][B]' + 2538 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + 2539 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + 2540 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + 2541 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + 2542 'datalist[A][B|option]' + 2543 'optgroup[A|disabled|label][option]' + 2544 'option[A|disabled|selected|label|value][]' + 2545 'textarea[A|autofocus|disabled|form|maxlength|name|placeholder|readonly|required|rows|cols|wrap][]' + 2546 'keygen[A|autofocus|challenge|disabled|form|keytype|name][]' + 2547 'output[A|for|form|name][B]' + 2548 'canvas[A|width|height][]' + 2549 'map[A|name][B|C]' + 2550 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' + 2551 'mathml[A][]' + 2552 'svg[A][]' + 2553 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' + 2554 'caption[A][C]' + 2555 'colgroup[A|span][col]' + 2556 'col[A|span][]' + 2557 'thead[A][tr]' + 2558 'tfoot[A][tr]' + 2559 'tbody[A][tr]' + 2560 'tr[A][th|td]' + 2561 'th[A|headers|rowspan|colspan|scope][B]' + 2562 'td[A|headers|rowspan|colspan][C]' + 2563 'wbr[A][]' 2564 ); 2565 } 2566 2567 return html5; 2568 }; 2569 2570 function getHTML4() { 2571 var html4 = mapCache.html4; 2572 2573 if (!html4) { 2574 // This is the XHTML 1.0 transitional elements with it's attributes and children packed to reduce it's size 2575 html4 = mapCache.html4 = unpack({ 2576 Z : 'H|K|N|O|P', 2577 Y : 'X|form|R|Q', 2578 ZG : 'E|span|width|align|char|charoff|valign', 2579 X : 'p|T|div|U|W|isindex|fieldset|table', 2580 ZF : 'E|align|char|charoff|valign', 2581 W : 'pre|hr|blockquote|address|center|noframes', 2582 ZE : 'abbr|axis|headers|scope|rowspan|colspan|align|char|charoff|valign|nowrap|bgcolor|width|height', 2583 ZD : '[E][S]', 2584 U : 'ul|ol|dl|menu|dir', 2585 ZC : 'p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q', 2586 T : 'h1|h2|h3|h4|h5|h6', 2587 ZB : 'X|S|Q', 2588 S : 'R|P', 2589 ZA : 'a|G|J|M|O|P', 2590 R : 'a|H|K|N|O', 2591 Q : 'noscript|P', 2592 P : 'ins|del|script', 2593 O : 'input|select|textarea|label|button', 2594 N : 'M|L', 2595 M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym', 2596 L : 'sub|sup', 2597 K : 'J|I', 2598 J : 'tt|i|b|u|s|strike', 2599 I : 'big|small|font|basefont', 2600 H : 'G|F', 2601 G : 'br|span|bdo', 2602 F : 'object|applet|img|map|iframe', 2603 E : 'A|B|C', 2604 D : 'accesskey|tabindex|onfocus|onblur', 2605 C : 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', 2606 B : 'lang|xml:lang|dir', 2607 A : 'id|class|style|title' 2608 }, 'script[id|charset|type|language|src|defer|xml:space][]' + 2609 'style[B|id|type|media|title|xml:space][]' + 2610 'object[E|declare|classid|codebase|data|type|codetype|archive|standby|width|height|usemap|name|tabindex|align|border|hspace|vspace][#|param|Y]' + 2611 'param[id|name|value|valuetype|type][]' + 2612 'p[E|align][#|S]' + 2613 'a[E|D|charset|type|name|href|hreflang|rel|rev|shape|coords|target][#|Z]' + 2614 'br[A|clear][]' + 2615 'span[E][#|S]' + 2616 'bdo[A|C|B][#|S]' + 2617 'applet[A|codebase|archive|code|object|alt|name|width|height|align|hspace|vspace][#|param|Y]' + 2618 'h1[E|align][#|S]' + 2619 'img[E|src|alt|name|longdesc|width|height|usemap|ismap|align|border|hspace|vspace][]' + 2620 'map[B|C|A|name][X|form|Q|area]' + 2621 'h2[E|align][#|S]' + 2622 'iframe[A|longdesc|name|src|frameborder|marginwidth|marginheight|scrolling|align|width|height][#|Y]' + 2623 'h3[E|align][#|S]' + 2624 'tt[E][#|S]' + 2625 'i[E][#|S]' + 2626 'b[E][#|S]' + 2627 'u[E][#|S]' + 2628 's[E][#|S]' + 2629 'strike[E][#|S]' + 2630 'big[E][#|S]' + 2631 'small[E][#|S]' + 2632 'font[A|B|size|color|face][#|S]' + 2633 'basefont[id|size|color|face][]' + 2634 'em[E][#|S]' + 2635 'strong[E][#|S]' + 2636 'dfn[E][#|S]' + 2637 'code[E][#|S]' + 2638 'q[E|cite][#|S]' + 2639 'samp[E][#|S]' + 2640 'kbd[E][#|S]' + 2641 'var[E][#|S]' + 2642 'cite[E][#|S]' + 2643 'abbr[E][#|S]' + 2644 'acronym[E][#|S]' + 2645 'sub[E][#|S]' + 2646 'sup[E][#|S]' + 2647 'input[E|D|type|name|value|checked|disabled|readonly|size|maxlength|src|alt|usemap|onselect|onchange|accept|align][]' + 2648 'select[E|name|size|multiple|disabled|tabindex|onfocus|onblur|onchange][optgroup|option]' + 2649 'optgroup[E|disabled|label][option]' + 2650 'option[E|selected|disabled|label|value][]' + 2651 'textarea[E|D|name|rows|cols|disabled|readonly|onselect|onchange][]' + 2652 'label[E|for|accesskey|onfocus|onblur][#|S]' + 2653 'button[E|D|name|value|type|disabled][#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' + 2654 'h4[E|align][#|S]' + 2655 'ins[E|cite|datetime][#|Y]' + 2656 'h5[E|align][#|S]' + 2657 'del[E|cite|datetime][#|Y]' + 2658 'h6[E|align][#|S]' + 2659 'div[E|align][#|Y]' + 2660 'ul[E|type|compact][li]' + 2661 'li[E|type|value][#|Y]' + 2662 'ol[E|type|compact|start][li]' + 2663 'dl[E|compact][dt|dd]' + 2664 'dt[E][#|S]' + 2665 'dd[E][#|Y]' + 2666 'menu[E|compact][li]' + 2667 'dir[E|compact][li]' + 2668 'pre[E|width|xml:space][#|ZA]' + 2669 'hr[E|align|noshade|size|width][]' + 2670 'blockquote[E|cite][#|Y]' + 2671 'address[E][#|S|p]' + 2672 'center[E][#|Y]' + 2673 'noframes[E][#|Y]' + 2674 'isindex[A|B|prompt][]' + 2675 'fieldset[E][#|legend|Y]' + 2676 'legend[E|accesskey|align][#|S]' + 2677 'table[E|summary|width|border|frame|rules|cellspacing|cellpadding|align|bgcolor][caption|col|colgroup|thead|tfoot|tbody|tr]' + 2678 'caption[E|align][#|S]' + 2679 'col[ZG][]' + 2680 'colgroup[ZG][col]' + 2681 'thead[ZF][tr]' + 2682 'tr[ZF|bgcolor][th|td]' + 2683 'th[E|ZE][#|Y]' + 2684 'form[E|action|method|name|enctype|onsubmit|onreset|accept|accept-charset|target][#|X|R|Q]' + 2685 'noscript[E][#|Y]' + 2686 'td[E|ZE][#|Y]' + 2687 'tfoot[ZF][tr]' + 2688 'tbody[ZF][tr]' + 2689 'area[E|D|shape|coords|href|nohref|alt|target][]' + 2690 'base[id|href|target][]' + 2691 'body[E|onload|onunload|background|bgcolor|text|link|vlink|alink][#|Y]' 2692 ); 2693 } 2694 2695 return html4; 2696 }; 2697 2698 tinymce.html.Schema = function(settings) { 2699 var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; 2700 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, blockElementsMap, nonEmptyElementsMap, customElementsMap = {}; 2701 2702 // Creates an lookup table map object for the specified option or the default value 2703 function createLookupTable(option, default_value, extend) { 2704 var value = settings[option]; 2705 2706 if (!value) { 2707 // Get cached default map or make it if needed 2708 value = mapCache[option]; 2709 2710 if (!value) { 2711 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 2712 value = tinymce.extend(value, extend); 2713 2714 mapCache[option] = value; 2715 } 2716 } else { 2717 // Create custom map 2718 value = makeMap(value, ',', makeMap(value.toUpperCase(), ' ')); 2719 } 2720 2721 return value; 2722 }; 2723 2724 settings = settings || {}; 2725 schemaItems = settings.schema == "html5" ? getHTML5() : getHTML4(); 2726 2727 // Allow all elements and attributes if verify_html is set to false 2728 if (settings.verify_html === false) 2729 settings.valid_elements = '*[*]'; 2730 2731 // Build styles list 2732 if (settings.valid_styles) { 2733 validStyles = {}; 2734 2735 // Convert styles into a rule list 2736 each(settings.valid_styles, function(value, key) { 2737 validStyles[key] = tinymce.explode(value); 2738 }); 2739 } 2740 2741 // Setup map objects 2742 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); 2743 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 2744 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); 2745 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); 2746 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); 2747 blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + 2748 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + 2749 'noscript menu isindex samp header footer article section hgroup aside nav figure option datalist select optgroup'); 2750 2751 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 2752 function patternToRegExp(str) { 2753 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 2754 }; 2755 2756 // Parses the specified valid_elements string and adds to the current rules 2757 // This function is a bit hard to read since it's heavily optimized for speed 2758 function addValidElements(valid_elements) { 2759 var ei, el, ai, al, yl, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 2760 prefix, outputName, globalAttributes, globalAttributesOrder, transElement, key, childKey, value, 2761 elementRuleRegExp = /^([#+\-])?([^\[\/]+)(?:\/([^\[]+))?(?:\[([^\]]+)\])?$/, 2762 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 2763 hasPatternsRegExp = /[*?+]/; 2764 2765 if (valid_elements) { 2766 // Split valid elements into an array with rules 2767 valid_elements = split(valid_elements); 2768 2769 if (elements['@']) { 2770 globalAttributes = elements['@'].attributes; 2771 globalAttributesOrder = elements['@'].attributesOrder; 2772 } 2773 2774 // Loop all rules 2775 for (ei = 0, el = valid_elements.length; ei < el; ei++) { 2776 // Parse element rule 2777 matches = elementRuleRegExp.exec(valid_elements[ei]); 2778 if (matches) { 2779 // Setup local names for matches 2780 prefix = matches[1]; 2781 elementName = matches[2]; 2782 outputName = matches[3]; 2783 attrData = matches[4]; 2784 2785 // Create new attributes and attributesOrder 2786 attributes = {}; 2787 attributesOrder = []; 2788 2789 // Create the new element 2790 element = { 2791 attributes : attributes, 2792 attributesOrder : attributesOrder 2793 }; 2794 2795 // Padd empty elements prefix 2796 if (prefix === '#') 2797 element.paddEmpty = true; 2798 2799 // Remove empty elements prefix 2800 if (prefix === '-') 2801 element.removeEmpty = true; 2802 2803 // Copy attributes from global rule into current rule 2804 if (globalAttributes) { 2805 for (key in globalAttributes) 2806 attributes[key] = globalAttributes[key]; 2807 2808 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 2809 } 2810 2811 // Attributes defined 2812 if (attrData) { 2813 attrData = split(attrData, '|'); 2814 for (ai = 0, al = attrData.length; ai < al; ai++) { 2815 matches = attrRuleRegExp.exec(attrData[ai]); 2816 if (matches) { 2817 attr = {}; 2818 attrType = matches[1]; 2819 attrName = matches[2].replace(/::/g, ':'); 2820 prefix = matches[3]; 2821 value = matches[4]; 2822 2823 // Required 2824 if (attrType === '!') { 2825 element.attributesRequired = element.attributesRequired || []; 2826 element.attributesRequired.push(attrName); 2827 attr.required = true; 2828 } 2829 2830 // Denied from global 2831 if (attrType === '-') { 2832 delete attributes[attrName]; 2833 attributesOrder.splice(tinymce.inArray(attributesOrder, attrName), 1); 2834 continue; 2835 } 2836 2837 // Default value 2838 if (prefix) { 2839 // Default value 2840 if (prefix === '=') { 2841 element.attributesDefault = element.attributesDefault || []; 2842 element.attributesDefault.push({name: attrName, value: value}); 2843 attr.defaultValue = value; 2844 } 2845 2846 // Forced value 2847 if (prefix === ':') { 2848 element.attributesForced = element.attributesForced || []; 2849 element.attributesForced.push({name: attrName, value: value}); 2850 attr.forcedValue = value; 2851 } 2852 2853 // Required values 2854 if (prefix === '<') 2855 attr.validValues = makeMap(value, '?'); 2856 } 2857 2858 // Check for attribute patterns 2859 if (hasPatternsRegExp.test(attrName)) { 2860 element.attributePatterns = element.attributePatterns || []; 2861 attr.pattern = patternToRegExp(attrName); 2862 element.attributePatterns.push(attr); 2863 } else { 2864 // Add attribute to order list if it doesn't already exist 2865 if (!attributes[attrName]) 2866 attributesOrder.push(attrName); 2867 2868 attributes[attrName] = attr; 2869 } 2870 } 2871 } 2872 } 2873 2874 // Global rule, store away these for later usage 2875 if (!globalAttributes && elementName == '@') { 2876 globalAttributes = attributes; 2877 globalAttributesOrder = attributesOrder; 2878 } 2879 2880 // Handle substitute elements such as b/strong 2881 if (outputName) { 2882 element.outputName = elementName; 2883 elements[outputName] = element; 2884 } 2885 2886 // Add pattern or exact element 2887 if (hasPatternsRegExp.test(elementName)) { 2888 element.pattern = patternToRegExp(elementName); 2889 patternElements.push(element); 2890 } else 2891 elements[elementName] = element; 2892 } 2893 } 2894 } 2895 }; 2896 2897 function setValidElements(valid_elements) { 2898 elements = {}; 2899 patternElements = []; 2900 2901 addValidElements(valid_elements); 2902 2903 each(schemaItems, function(element, name) { 2904 children[name] = element.children; 2905 }); 2906 }; 2907 2908 // Adds custom non HTML elements to the schema 2909 function addCustomElements(custom_elements) { 2910 var customElementRegExp = /^(~)?(.+)$/; 2911 2912 if (custom_elements) { 2913 each(split(custom_elements), function(rule) { 2914 var matches = customElementRegExp.exec(rule), 2915 inline = matches[1] === '~', 2916 cloneName = inline ? 'span' : 'div', 2917 name = matches[2]; 2918 2919 children[name] = children[cloneName]; 2920 customElementsMap[name] = cloneName; 2921 2922 // If it's not marked as inline then add it to valid block elements 2923 if (!inline) 2924 blockElementsMap[name] = {}; 2925 2926 // Add custom elements at span/div positions 2927 each(children, function(element, child) { 2928 if (element[cloneName]) 2929 element[name] = element[cloneName]; 2930 }); 2931 }); 2932 } 2933 }; 2934 2935 // Adds valid children to the schema object 2936 function addValidChildren(valid_children) { 2937 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 2938 2939 if (valid_children) { 2940 each(split(valid_children), function(rule) { 2941 var matches = childRuleRegExp.exec(rule), parent, prefix; 2942 2943 if (matches) { 2944 prefix = matches[1]; 2945 2946 // Add/remove items from default 2947 if (prefix) 2948 parent = children[matches[2]]; 2949 else 2950 parent = children[matches[2]] = {'#comment' : {}}; 2951 2952 parent = children[matches[2]]; 2953 2954 each(split(matches[3], '|'), function(child) { 2955 if (prefix === '-') 2956 delete parent[child]; 2957 else 2958 parent[child] = {}; 2959 }); 2960 } 2961 }); 2962 } 2963 }; 2964 2965 function getElementRule(name) { 2966 var element = elements[name], i; 2967 2968 // Exact match found 2969 if (element) 2970 return element; 2971 2972 // No exact match then try the patterns 2973 i = patternElements.length; 2974 while (i--) { 2975 element = patternElements[i]; 2976 2977 if (element.pattern.test(name)) 2978 return element; 2979 } 2980 }; 2981 2982 if (!settings.valid_elements) { 2983 // No valid elements defined then clone the elements from the schema spec 2984 each(schemaItems, function(element, name) { 2985 elements[name] = { 2986 attributes : element.attributes, 2987 attributesOrder : element.attributesOrder 2988 }; 2989 2990 children[name] = element.children; 2991 }); 2992 2993 // Switch these on HTML4 2994 if (settings.schema != "html5") { 2995 each(split('strong/b,em/i'), function(item) { 2996 item = split(item, '/'); 2997 elements[item[1]].outputName = item[0]; 2998 }); 2999 } 3000 3001 // Add default alt attribute for images 3002 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 3003 3004 // Remove these if they are empty by default 3005 each(split('ol,ul,sub,sup,blockquote,span,font,a,table,tbody,tr,strong,em,b,i'), function(name) { 3006 if (elements[name]) { 3007 elements[name].removeEmpty = true; 3008 } 3009 }); 3010 3011 // Padd these by default 3012 each(split('p,h1,h2,h3,h4,h5,h6,th,td,pre,div,address,caption'), function(name) { 3013 elements[name].paddEmpty = true; 3014 }); 3015 } else 3016 setValidElements(settings.valid_elements); 3017 3018 addCustomElements(settings.custom_elements); 3019 addValidChildren(settings.valid_children); 3020 addValidElements(settings.extended_valid_elements); 3021 3022 // Todo: Remove this when we fix list handling to be valid 3023 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 3024 3025 // Delete invalid elements 3026 if (settings.invalid_elements) { 3027 tinymce.each(tinymce.explode(settings.invalid_elements), function(item) { 3028 if (elements[item]) 3029 delete elements[item]; 3030 }); 3031 } 3032 3033 // If the user didn't allow span only allow internal spans 3034 if (!getElementRule('span')) 3035 addValidElements('span[!data-mce-type|*]'); 3036 3037 self.children = children; 3038 3039 self.styles = validStyles; 3040 3041 self.getBoolAttrs = function() { 3042 return boolAttrMap; 3043 }; 3044 3045 self.getBlockElements = function() { 3046 return blockElementsMap; 3047 }; 3048 3049 self.getShortEndedElements = function() { 3050 return shortEndedElementsMap; 3051 }; 3052 3053 self.getSelfClosingElements = function() { 3054 return selfClosingElementsMap; 3055 }; 3056 3057 self.getNonEmptyElements = function() { 3058 return nonEmptyElementsMap; 3059 }; 3060 3061 self.getWhiteSpaceElements = function() { 3062 return whiteSpaceElementsMap; 3063 }; 3064 3065 self.isValidChild = function(name, child) { 3066 var parent = children[name]; 3067 3068 return !!(parent && parent[child]); 3069 }; 3070 3071 self.isValid = function(name, attr) { 3072 var attrPatterns, i, rule = getElementRule(name); 3073 3074 // Check if it's a valid element 3075 if (rule) { 3076 if (attr) { 3077 // Check if attribute name exists 3078 if (rule.attributes[attr]) { 3079 return true; 3080 } 3081 3082 // Check if attribute matches a regexp pattern 3083 attrPatterns = rule.attributePatterns; 3084 if (attrPatterns) { 3085 i = attrPatterns.length; 3086 while (i--) { 3087 if (attrPatterns[i].pattern.test(name)) { 3088 return true; 3089 } 3090 } 3091 } 3092 } else { 3093 return true; 3094 } 3095 } 3096 3097 // No match 3098 return false; 3099 }; 3100 3101 self.getElementRule = getElementRule; 3102 3103 self.getCustomElements = function() { 3104 return customElementsMap; 3105 }; 3106 3107 self.addValidElements = addValidElements; 3108 3109 self.setValidElements = setValidElements; 3110 3111 self.addCustomElements = addCustomElements; 3112 3113 self.addValidChildren = addValidChildren; 3114 }; 3115 })(tinymce); 3116 3117 (function(tinymce) { 3118 tinymce.html.SaxParser = function(settings, schema) { 3119 var self = this, noop = function() {}; 3120 3121 settings = settings || {}; 3122 self.schema = schema = schema || new tinymce.html.Schema(); 3123 3124 if (settings.fix_self_closing !== false) 3125 settings.fix_self_closing = true; 3126 3127 // Add handler functions from settings and setup default handlers 3128 tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { 3129 if (name) 3130 self[name] = settings[name] || noop; 3131 }); 3132 3133 self.parse = function(html) { 3134 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, 3135 shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, 3136 validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, 3137 tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; 3138 3139 function processEndTag(name) { 3140 var pos, i; 3141 3142 // Find position of parent of the same type 3143 pos = stack.length; 3144 while (pos--) { 3145 if (stack[pos].name === name) 3146 break; 3147 } 3148 3149 // Found parent 3150 if (pos >= 0) { 3151 // Close all the open elements 3152 for (i = stack.length - 1; i >= pos; i--) { 3153 name = stack[i]; 3154 3155 if (name.valid) 3156 self.end(name.name); 3157 } 3158 3159 // Remove the open elements from the stack 3160 stack.length = pos; 3161 } 3162 }; 3163 3164 function parseAttribute(match, name, value, val2, val3) { 3165 var attrRule, i; 3166 3167 name = name.toLowerCase(); 3168 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 3169 3170 // Validate name and value 3171 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 3172 attrRule = validAttributesMap[name]; 3173 3174 // Find rule by pattern matching 3175 if (!attrRule && validAttributePatterns) { 3176 i = validAttributePatterns.length; 3177 while (i--) { 3178 attrRule = validAttributePatterns[i]; 3179 if (attrRule.pattern.test(name)) 3180 break; 3181 } 3182 3183 // No rule matched 3184 if (i === -1) 3185 attrRule = null; 3186 } 3187 3188 // No attribute rule found 3189 if (!attrRule) 3190 return; 3191 3192 // Validate value 3193 if (attrRule.validValues && !(value in attrRule.validValues)) 3194 return; 3195 } 3196 3197 // Add attribute to list and map 3198 attrList.map[name] = value; 3199 attrList.push({ 3200 name: name, 3201 value: value 3202 }); 3203 }; 3204 3205 // Precompile RegExps and map objects 3206 tokenRegExp = new RegExp('<(?:' + 3207 '(?:!--([\\w\\W]*?)-->)|' + // Comment 3208 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 3209 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 3210 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 3211 '(?:\\/([^>]+)>)|' + // End element 3212 '(?:([A-Za-z0-9\\-\\:]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 3213 ')', 'g'); 3214 3215 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:\\.|[^\"])*)\")|(?:\'((?:\\.|[^\'])*)\')|([^>\s]+)))?/g; 3216 specialElements = { 3217 'script' : /<\/script[^>]*>/gi, 3218 'style' : /<\/style[^>]*>/gi, 3219 'noscript' : /<\/noscript[^>]*>/gi 3220 }; 3221 3222 // Setup lookup tables for empty elements and boolean attributes 3223 shortEndedElements = schema.getShortEndedElements(); 3224 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 3225 fillAttrsMap = schema.getBoolAttrs(); 3226 validate = settings.validate; 3227 removeInternalElements = settings.remove_internals; 3228 fixSelfClosing = settings.fix_self_closing; 3229 isIE = tinymce.isIE; 3230 invalidPrefixRegExp = /^:/; 3231 3232 while (matches = tokenRegExp.exec(html)) { 3233 // Text 3234 if (index < matches.index) 3235 self.text(decode(html.substr(index, matches.index - index))); 3236 3237 if (value = matches[6]) { // End element 3238 value = value.toLowerCase(); 3239 3240 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3241 if (isIE && invalidPrefixRegExp.test(value)) 3242 value = value.substr(1); 3243 3244 processEndTag(value); 3245 } else if (value = matches[7]) { // Start element 3246 value = value.toLowerCase(); 3247 3248 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 3249 if (isIE && invalidPrefixRegExp.test(value)) 3250 value = value.substr(1); 3251 3252 isShortEnded = value in shortEndedElements; 3253 3254 // Is self closing tag for example an <li> after an open <li> 3255 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) 3256 processEndTag(value); 3257 3258 // Validate element 3259 if (!validate || (elementRule = schema.getElementRule(value))) { 3260 isValidElement = true; 3261 3262 // Grab attributes map and patters when validation is enabled 3263 if (validate) { 3264 validAttributesMap = elementRule.attributes; 3265 validAttributePatterns = elementRule.attributePatterns; 3266 } 3267 3268 // Parse attributes 3269 if (attribsValue = matches[8]) { 3270 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 3271 3272 // If the element has internal attributes then remove it if we are told to do so 3273 if (isInternalElement && removeInternalElements) 3274 isValidElement = false; 3275 3276 attrList = []; 3277 attrList.map = {}; 3278 3279 attribsValue.replace(attrRegExp, parseAttribute); 3280 } else { 3281 attrList = []; 3282 attrList.map = {}; 3283 } 3284 3285 // Process attributes if validation is enabled 3286 if (validate && !isInternalElement) { 3287 attributesRequired = elementRule.attributesRequired; 3288 attributesDefault = elementRule.attributesDefault; 3289 attributesForced = elementRule.attributesForced; 3290 3291 // Handle forced attributes 3292 if (attributesForced) { 3293 i = attributesForced.length; 3294 while (i--) { 3295 attr = attributesForced[i]; 3296 name = attr.name; 3297 attrValue = attr.value; 3298 3299 if (attrValue === '{$uid}') 3300 attrValue = 'mce_' + idCount++; 3301 3302 attrList.map[name] = attrValue; 3303 attrList.push({name: name, value: attrValue}); 3304 } 3305 } 3306 3307 // Handle default attributes 3308 if (attributesDefault) { 3309 i = attributesDefault.length; 3310 while (i--) { 3311 attr = attributesDefault[i]; 3312 name = attr.name; 3313 3314 if (!(name in attrList.map)) { 3315 attrValue = attr.value; 3316 3317 if (attrValue === '{$uid}') 3318 attrValue = 'mce_' + idCount++; 3319 3320 attrList.map[name] = attrValue; 3321 attrList.push({name: name, value: attrValue}); 3322 } 3323 } 3324 } 3325 3326 // Handle required attributes 3327 if (attributesRequired) { 3328 i = attributesRequired.length; 3329 while (i--) { 3330 if (attributesRequired[i] in attrList.map) 3331 break; 3332 } 3333 3334 // None of the required attributes where found 3335 if (i === -1) 3336 isValidElement = false; 3337 } 3338 3339 // Invalidate element if it's marked as bogus 3340 if (attrList.map['data-mce-bogus']) 3341 isValidElement = false; 3342 } 3343 3344 if (isValidElement) 3345 self.start(value, attrList, isShortEnded); 3346 } else 3347 isValidElement = false; 3348 3349 // Treat script, noscript and style a bit different since they may include code that looks like elements 3350 if (endRegExp = specialElements[value]) { 3351 endRegExp.lastIndex = index = matches.index + matches[0].length; 3352 3353 if (matches = endRegExp.exec(html)) { 3354 if (isValidElement) 3355 text = html.substr(index, matches.index - index); 3356 3357 index = matches.index + matches[0].length; 3358 } else { 3359 text = html.substr(index); 3360 index = html.length; 3361 } 3362 3363 if (isValidElement && text.length > 0) 3364 self.text(text, true); 3365 3366 if (isValidElement) 3367 self.end(value); 3368 3369 tokenRegExp.lastIndex = index; 3370 continue; 3371 } 3372 3373 // Push value on to stack 3374 if (!isShortEnded) { 3375 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) 3376 stack.push({name: value, valid: isValidElement}); 3377 else if (isValidElement) 3378 self.end(value); 3379 } 3380 } else if (value = matches[1]) { // Comment 3381 self.comment(value); 3382 } else if (value = matches[2]) { // CDATA 3383 self.cdata(value); 3384 } else if (value = matches[3]) { // DOCTYPE 3385 self.doctype(value); 3386 } else if (value = matches[4]) { // PI 3387 self.pi(value, matches[5]); 3388 } 3389 3390 index = matches.index + matches[0].length; 3391 } 3392 3393 // Text 3394 if (index < html.length) 3395 self.text(decode(html.substr(index))); 3396 3397 // Close any open elements 3398 for (i = stack.length - 1; i >= 0; i--) { 3399 value = stack[i]; 3400 3401 if (value.valid) 3402 self.end(value.name); 3403 } 3404 }; 3405 } 3406 })(tinymce); 3407 3408 (function(tinymce) { 3409 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 3410 '#text' : 3, 3411 '#comment' : 8, 3412 '#cdata' : 4, 3413 '#pi' : 7, 3414 '#doctype' : 10, 3415 '#document-fragment' : 11 3416 }; 3417 3418 // Walks the tree left/right 3419 function walk(node, root_node, prev) { 3420 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 3421 3422 // Walk into nodes if it has a start 3423 if (node[startName]) 3424 return node[startName]; 3425 3426 // Return the sibling if it has one 3427 if (node !== root_node) { 3428 sibling = node[siblingName]; 3429 3430 if (sibling) 3431 return sibling; 3432 3433 // Walk up the parents to look for siblings 3434 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 3435 sibling = parent[siblingName]; 3436 3437 if (sibling) 3438 return sibling; 3439 } 3440 } 3441 }; 3442 3443 function Node(name, type) { 3444 this.name = name; 3445 this.type = type; 3446 3447 if (type === 1) { 3448 this.attributes = []; 3449 this.attributes.map = {}; 3450 } 3451 } 3452 3453 tinymce.extend(Node.prototype, { 3454 replace : function(node) { 3455 var self = this; 3456 3457 if (node.parent) 3458 node.remove(); 3459 3460 self.insert(node, self); 3461 self.remove(); 3462 3463 return self; 3464 }, 3465 3466 attr : function(name, value) { 3467 var self = this, attrs, i, undef; 3468 3469 if (typeof name !== "string") { 3470 for (i in name) 3471 self.attr(i, name[i]); 3472 3473 return self; 3474 } 3475 3476 if (attrs = self.attributes) { 3477 if (value !== undef) { 3478 // Remove attribute 3479 if (value === null) { 3480 if (name in attrs.map) { 3481 delete attrs.map[name]; 3482 3483 i = attrs.length; 3484 while (i--) { 3485 if (attrs[i].name === name) { 3486 attrs = attrs.splice(i, 1); 3487 return self; 3488 } 3489 } 3490 } 3491 3492 return self; 3493 } 3494 3495 // Set attribute 3496 if (name in attrs.map) { 3497 // Set attribute 3498 i = attrs.length; 3499 while (i--) { 3500 if (attrs[i].name === name) { 3501 attrs[i].value = value; 3502 break; 3503 } 3504 } 3505 } else 3506 attrs.push({name: name, value: value}); 3507 3508 attrs.map[name] = value; 3509 3510 return self; 3511 } else { 3512 return attrs.map[name]; 3513 } 3514 } 3515 }, 3516 3517 clone : function() { 3518 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 3519 3520 // Clone element attributes 3521 if (selfAttrs = self.attributes) { 3522 cloneAttrs = []; 3523 cloneAttrs.map = {}; 3524 3525 for (i = 0, l = selfAttrs.length; i < l; i++) { 3526 selfAttr = selfAttrs[i]; 3527 3528 // Clone everything except id 3529 if (selfAttr.name !== 'id') { 3530 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 3531 cloneAttrs.map[selfAttr.name] = selfAttr.value; 3532 } 3533 } 3534 3535 clone.attributes = cloneAttrs; 3536 } 3537 3538 clone.value = self.value; 3539 clone.shortEnded = self.shortEnded; 3540 3541 return clone; 3542 }, 3543 3544 wrap : function(wrapper) { 3545 var self = this; 3546 3547 self.parent.insert(wrapper, self); 3548 wrapper.append(self); 3549 3550 return self; 3551 }, 3552 3553 unwrap : function() { 3554 var self = this, node, next; 3555 3556 for (node = self.firstChild; node; ) { 3557 next = node.next; 3558 self.insert(node, self, true); 3559 node = next; 3560 } 3561 3562 self.remove(); 3563 }, 3564 3565 remove : function() { 3566 var self = this, parent = self.parent, next = self.next, prev = self.prev; 3567 3568 if (parent) { 3569 if (parent.firstChild === self) { 3570 parent.firstChild = next; 3571 3572 if (next) 3573 next.prev = null; 3574 } else { 3575 prev.next = next; 3576 } 3577 3578 if (parent.lastChild === self) { 3579 parent.lastChild = prev; 3580 3581 if (prev) 3582 prev.next = null; 3583 } else { 3584 next.prev = prev; 3585 } 3586 3587 self.parent = self.next = self.prev = null; 3588 } 3589 3590 return self; 3591 }, 3592 3593 append : function(node) { 3594 var self = this, last; 3595 3596 if (node.parent) 3597 node.remove(); 3598 3599 last = self.lastChild; 3600 if (last) { 3601 last.next = node; 3602 node.prev = last; 3603 self.lastChild = node; 3604 } else 3605 self.lastChild = self.firstChild = node; 3606 3607 node.parent = self; 3608 3609 return node; 3610 }, 3611 3612 insert : function(node, ref_node, before) { 3613 var parent; 3614 3615 if (node.parent) 3616 node.remove(); 3617 3618 parent = ref_node.parent || this; 3619 3620 if (before) { 3621 if (ref_node === parent.firstChild) 3622 parent.firstChild = node; 3623 else 3624 ref_node.prev.next = node; 3625 3626 node.prev = ref_node.prev; 3627 node.next = ref_node; 3628 ref_node.prev = node; 3629 } else { 3630 if (ref_node === parent.lastChild) 3631 parent.lastChild = node; 3632 else 3633 ref_node.next.prev = node; 3634 3635 node.next = ref_node.next; 3636 node.prev = ref_node; 3637 ref_node.next = node; 3638 } 3639 3640 node.parent = parent; 3641 3642 return node; 3643 }, 3644 3645 getAll : function(name) { 3646 var self = this, node, collection = []; 3647 3648 for (node = self.firstChild; node; node = walk(node, self)) { 3649 if (node.name === name) 3650 collection.push(node); 3651 } 3652 3653 return collection; 3654 }, 3655 3656 empty : function() { 3657 var self = this, nodes, i, node; 3658 3659 // Remove all children 3660 if (self.firstChild) { 3661 nodes = []; 3662 3663 // Collect the children 3664 for (node = self.firstChild; node; node = walk(node, self)) 3665 nodes.push(node); 3666 3667 // Remove the children 3668 i = nodes.length; 3669 while (i--) { 3670 node = nodes[i]; 3671 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 3672 } 3673 } 3674 3675 self.firstChild = self.lastChild = null; 3676 3677 return self; 3678 }, 3679 3680 isEmpty : function(elements) { 3681 var self = this, node = self.firstChild, i, name; 3682 3683 if (node) { 3684 do { 3685 if (node.type === 1) { 3686 // Ignore bogus elements 3687 if (node.attributes.map['data-mce-bogus']) 3688 continue; 3689 3690 // Keep empty elements like <img /> 3691 if (elements[node.name]) 3692 return false; 3693 3694 // Keep elements with data attributes or name attribute like <a name="1"></a> 3695 i = node.attributes.length; 3696 while (i--) { 3697 name = node.attributes[i].name; 3698 if (name === "name" || name.indexOf('data-') === 0) 3699 return false; 3700 } 3701 } 3702 3703 // Keep comments 3704 if (node.type === 8) 3705 return false; 3706 3707 // Keep non whitespace text nodes 3708 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) 3709 return false; 3710 } while (node = walk(node, self)); 3711 } 3712 3713 return true; 3714 }, 3715 3716 walk : function(prev) { 3717 return walk(this, null, prev); 3718 } 3719 }); 3720 3721 tinymce.extend(Node, { 3722 create : function(name, attrs) { 3723 var node, attrName; 3724 3725 // Create node 3726 node = new Node(name, typeLookup[name] || 1); 3727 3728 // Add attributes if needed 3729 if (attrs) { 3730 for (attrName in attrs) 3731 node.attr(attrName, attrs[attrName]); 3732 } 3733 3734 return node; 3735 } 3736 }); 3737 3738 tinymce.html.Node = Node; 3739 })(tinymce); 3740 3741 (function(tinymce) { 3742 var Node = tinymce.html.Node; 3743 3744 tinymce.html.DomParser = function(settings, schema) { 3745 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 3746 3747 settings = settings || {}; 3748 settings.validate = "validate" in settings ? settings.validate : true; 3749 settings.root_name = settings.root_name || 'body'; 3750 self.schema = schema = schema || new tinymce.html.Schema(); 3751 3752 function fixInvalidChildren(nodes) { 3753 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i, 3754 childClone, nonEmptyElements, nonSplitableElements, sibling, nextNode; 3755 3756 nonSplitableElements = tinymce.makeMap('tr,td,th,tbody,thead,tfoot,table'); 3757 nonEmptyElements = schema.getNonEmptyElements(); 3758 3759 for (ni = 0; ni < nodes.length; ni++) { 3760 node = nodes[ni]; 3761 3762 // Already removed 3763 if (!node.parent) 3764 continue; 3765 3766 // Get list of all parent nodes until we find a valid parent to stick the child into 3767 parents = [node]; 3768 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && !nonSplitableElements[parent.name]; parent = parent.parent) 3769 parents.push(parent); 3770 3771 // Found a suitable parent 3772 if (parent && parents.length > 1) { 3773 // Reverse the array since it makes looping easier 3774 parents.reverse(); 3775 3776 // Clone the related parent and insert that after the moved node 3777 newParent = currentNode = self.filterNode(parents[0].clone()); 3778 3779 // Start cloning and moving children on the left side of the target node 3780 for (i = 0; i < parents.length - 1; i++) { 3781 if (schema.isValidChild(currentNode.name, parents[i].name)) { 3782 tempNode = self.filterNode(parents[i].clone()); 3783 currentNode.append(tempNode); 3784 } else 3785 tempNode = currentNode; 3786 3787 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { 3788 nextNode = childNode.next; 3789 tempNode.append(childNode); 3790 childNode = nextNode; 3791 } 3792 3793 currentNode = tempNode; 3794 } 3795 3796 if (!newParent.isEmpty(nonEmptyElements)) { 3797 parent.insert(newParent, parents[0], true); 3798 parent.insert(node, newParent); 3799 } else { 3800 parent.insert(node, parents[0], true); 3801 } 3802 3803 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 3804 parent = parents[0]; 3805 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 3806 parent.empty().remove(); 3807 } 3808 } else if (node.parent) { 3809 // If it's an LI try to find a UL/OL for it or wrap it 3810 if (node.name === 'li') { 3811 sibling = node.prev; 3812 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3813 sibling.append(node); 3814 continue; 3815 } 3816 3817 sibling = node.next; 3818 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 3819 sibling.insert(node, sibling.firstChild, true); 3820 continue; 3821 } 3822 3823 node.wrap(self.filterNode(new Node('ul', 1))); 3824 continue; 3825 } 3826 3827 // Try wrapping the element in a DIV 3828 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 3829 node.wrap(self.filterNode(new Node('div', 1))); 3830 } else { 3831 // We failed wrapping it, then remove or unwrap it 3832 if (node.name === 'style' || node.name === 'script') 3833 node.empty().remove(); 3834 else 3835 node.unwrap(); 3836 } 3837 } 3838 } 3839 }; 3840 3841 self.filterNode = function(node) { 3842 var i, name, list; 3843 3844 // Run element filters 3845 if (name in nodeFilters) { 3846 list = matchedNodes[name]; 3847 3848 if (list) 3849 list.push(node); 3850 else 3851 matchedNodes[name] = [node]; 3852 } 3853 3854 // Run attribute filters 3855 i = attributeFilters.length; 3856 while (i--) { 3857 name = attributeFilters[i].name; 3858 3859 if (name in node.attributes.map) { 3860 list = matchedAttributes[name]; 3861 3862 if (list) 3863 list.push(node); 3864 else 3865 matchedAttributes[name] = [node]; 3866 } 3867 } 3868 3869 return node; 3870 }; 3871 3872 self.addNodeFilter = function(name, callback) { 3873 tinymce.each(tinymce.explode(name), function(name) { 3874 var list = nodeFilters[name]; 3875 3876 if (!list) 3877 nodeFilters[name] = list = []; 3878 3879 list.push(callback); 3880 }); 3881 }; 3882 3883 self.addAttributeFilter = function(name, callback) { 3884 tinymce.each(tinymce.explode(name), function(name) { 3885 var i; 3886 3887 for (i = 0; i < attributeFilters.length; i++) { 3888 if (attributeFilters[i].name === name) { 3889 attributeFilters[i].callbacks.push(callback); 3890 return; 3891 } 3892 } 3893 3894 attributeFilters.push({name: name, callbacks: [callback]}); 3895 }); 3896 }; 3897 3898 self.parse = function(html, args) { 3899 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate, 3900 blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement, 3901 endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements, children, nonEmptyElements, rootBlockName; 3902 3903 args = args || {}; 3904 matchedNodes = {}; 3905 matchedAttributes = {}; 3906 blockElements = tinymce.extend(tinymce.makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 3907 nonEmptyElements = schema.getNonEmptyElements(); 3908 children = schema.children; 3909 validate = settings.validate; 3910 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 3911 3912 whiteSpaceElements = schema.getWhiteSpaceElements(); 3913 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 3914 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 3915 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 3916 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 3917 3918 function addRootBlocks() { 3919 var node = rootNode.firstChild, next, rootBlockNode; 3920 3921 while (node) { 3922 next = node.next; 3923 3924 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { 3925 if (!rootBlockNode) { 3926 // Create a new root block element 3927 rootBlockNode = createNode(rootBlockName, 1); 3928 rootNode.insert(rootBlockNode, node); 3929 rootBlockNode.append(node); 3930 } else 3931 rootBlockNode.append(node); 3932 } else { 3933 rootBlockNode = null; 3934 } 3935 3936 node = next; 3937 }; 3938 }; 3939 3940 function createNode(name, type) { 3941 var node = new Node(name, type), list; 3942 3943 if (name in nodeFilters) { 3944 list = matchedNodes[name]; 3945 3946 if (list) 3947 list.push(node); 3948 else 3949 matchedNodes[name] = [node]; 3950 } 3951 3952 return node; 3953 }; 3954 3955 function removeWhitespaceBefore(node) { 3956 var textNode, textVal, sibling; 3957 3958 for (textNode = node.prev; textNode && textNode.type === 3; ) { 3959 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 3960 3961 if (textVal.length > 0) { 3962 textNode.value = textVal; 3963 textNode = textNode.prev; 3964 } else { 3965 sibling = textNode.prev; 3966 textNode.remove(); 3967 textNode = sibling; 3968 } 3969 } 3970 }; 3971 3972 function cloneAndExcludeBlocks(input) { 3973 var name, output = {}; 3974 3975 for (name in input) { 3976 if (name !== 'li' && name != 'p') { 3977 output[name] = input[name]; 3978 } 3979 } 3980 3981 return output; 3982 }; 3983 3984 parser = new tinymce.html.SaxParser({ 3985 validate : validate, 3986 3987 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 3988 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 3989 3990 cdata: function(text) { 3991 node.append(createNode('#cdata', 4)).value = text; 3992 }, 3993 3994 text: function(text, raw) { 3995 var textNode; 3996 3997 // Trim all redundant whitespace on non white space elements 3998 if (!isInWhiteSpacePreservedElement) { 3999 text = text.replace(allWhiteSpaceRegExp, ' '); 4000 4001 if (node.lastChild && blockElements[node.lastChild.name]) 4002 text = text.replace(startWhiteSpaceRegExp, ''); 4003 } 4004 4005 // Do we need to create the node 4006 if (text.length !== 0) { 4007 textNode = createNode('#text', 3); 4008 textNode.raw = !!raw; 4009 node.append(textNode).value = text; 4010 } 4011 }, 4012 4013 comment: function(text) { 4014 node.append(createNode('#comment', 8)).value = text; 4015 }, 4016 4017 pi: function(name, text) { 4018 node.append(createNode(name, 7)).value = text; 4019 removeWhitespaceBefore(node); 4020 }, 4021 4022 doctype: function(text) { 4023 var newNode; 4024 4025 newNode = node.append(createNode('#doctype', 10)); 4026 newNode.value = text; 4027 removeWhitespaceBefore(node); 4028 }, 4029 4030 start: function(name, attrs, empty) { 4031 var newNode, attrFiltersLen, elementRule, textNode, attrName, text, sibling, parent; 4032 4033 elementRule = validate ? schema.getElementRule(name) : {}; 4034 if (elementRule) { 4035 newNode = createNode(elementRule.outputName || name, 1); 4036 newNode.attributes = attrs; 4037 newNode.shortEnded = empty; 4038 4039 node.append(newNode); 4040 4041 // Check if node is valid child of the parent node is the child is 4042 // unknown we don't collect it since it's probably a custom element 4043 parent = children[node.name]; 4044 if (parent && children[newNode.name] && !parent[newNode.name]) 4045 invalidChildren.push(newNode); 4046 4047 attrFiltersLen = attributeFilters.length; 4048 while (attrFiltersLen--) { 4049 attrName = attributeFilters[attrFiltersLen].name; 4050 4051 if (attrName in attrs.map) { 4052 list = matchedAttributes[attrName]; 4053 4054 if (list) 4055 list.push(newNode); 4056 else 4057 matchedAttributes[attrName] = [newNode]; 4058 } 4059 } 4060 4061 // Trim whitespace before block 4062 if (blockElements[name]) 4063 removeWhitespaceBefore(newNode); 4064 4065 // Change current node if the element wasn't empty i.e not <br /> or <img /> 4066 if (!empty) 4067 node = newNode; 4068 4069 // Check if we are inside a whitespace preserved element 4070 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4071 isInWhiteSpacePreservedElement = true; 4072 } 4073 } 4074 }, 4075 4076 end: function(name) { 4077 var textNode, elementRule, text, sibling, tempNode; 4078 4079 elementRule = validate ? schema.getElementRule(name) : {}; 4080 if (elementRule) { 4081 if (blockElements[name]) { 4082 if (!isInWhiteSpacePreservedElement) { 4083 // Trim whitespace of the first node in a block 4084 textNode = node.firstChild; 4085 if (textNode && textNode.type === 3) { 4086 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4087 4088 // Any characters left after trim or should we remove it 4089 if (text.length > 0) { 4090 textNode.value = text; 4091 textNode = textNode.next; 4092 } else { 4093 sibling = textNode.next; 4094 textNode.remove(); 4095 textNode = sibling; 4096 } 4097 4098 // Remove any pure whitespace siblings 4099 while (textNode && textNode.type === 3) { 4100 text = textNode.value; 4101 sibling = textNode.next; 4102 4103 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4104 textNode.remove(); 4105 textNode = sibling; 4106 } 4107 4108 textNode = sibling; 4109 } 4110 } 4111 4112 // Trim whitespace of the last node in a block 4113 textNode = node.lastChild; 4114 if (textNode && textNode.type === 3) { 4115 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 4116 4117 // Any characters left after trim or should we remove it 4118 if (text.length > 0) { 4119 textNode.value = text; 4120 textNode = textNode.prev; 4121 } else { 4122 sibling = textNode.prev; 4123 textNode.remove(); 4124 textNode = sibling; 4125 } 4126 4127 // Remove any pure whitespace siblings 4128 while (textNode && textNode.type === 3) { 4129 text = textNode.value; 4130 sibling = textNode.prev; 4131 4132 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 4133 textNode.remove(); 4134 textNode = sibling; 4135 } 4136 4137 textNode = sibling; 4138 } 4139 } 4140 } 4141 4142 // Trim start white space 4143 textNode = node.prev; 4144 if (textNode && textNode.type === 3) { 4145 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 4146 4147 if (text.length > 0) 4148 textNode.value = text; 4149 else 4150 textNode.remove(); 4151 } 4152 } 4153 4154 // Check if we exited a whitespace preserved element 4155 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 4156 isInWhiteSpacePreservedElement = false; 4157 } 4158 4159 // Handle empty nodes 4160 if (elementRule.removeEmpty || elementRule.paddEmpty) { 4161 if (node.isEmpty(nonEmptyElements)) { 4162 if (elementRule.paddEmpty) 4163 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 4164 else { 4165 // Leave nodes that have a name like <a name="name"> 4166 if (!node.attributes.map.name && !node.attributes.map.id) { 4167 tempNode = node.parent; 4168 node.empty().remove(); 4169 node = tempNode; 4170 return; 4171 } 4172 } 4173 } 4174 } 4175 4176 node = node.parent; 4177 } 4178 } 4179 }, schema); 4180 4181 rootNode = node = new Node(args.context || settings.root_name, 11); 4182 4183 parser.parse(html); 4184 4185 // Fix invalid children or report invalid children in a contextual parsing 4186 if (validate && invalidChildren.length) { 4187 if (!args.context) 4188 fixInvalidChildren(invalidChildren); 4189 else 4190 args.invalid = true; 4191 } 4192 4193 // Wrap nodes in the root into block elements if the root is body 4194 if (rootBlockName && rootNode.name == 'body') 4195 addRootBlocks(); 4196 4197 // Run filters only when the contents is valid 4198 if (!args.invalid) { 4199 // Run node filters 4200 for (name in matchedNodes) { 4201 list = nodeFilters[name]; 4202 nodes = matchedNodes[name]; 4203 4204 // Remove already removed children 4205 fi = nodes.length; 4206 while (fi--) { 4207 if (!nodes[fi].parent) 4208 nodes.splice(fi, 1); 4209 } 4210 4211 for (i = 0, l = list.length; i < l; i++) 4212 list[i](nodes, name, args); 4213 } 4214 4215 // Run attribute filters 4216 for (i = 0, l = attributeFilters.length; i < l; i++) { 4217 list = attributeFilters[i]; 4218 4219 if (list.name in matchedAttributes) { 4220 nodes = matchedAttributes[list.name]; 4221 4222 // Remove already removed children 4223 fi = nodes.length; 4224 while (fi--) { 4225 if (!nodes[fi].parent) 4226 nodes.splice(fi, 1); 4227 } 4228 4229 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) 4230 list.callbacks[fi](nodes, list.name, args); 4231 } 4232 } 4233 } 4234 4235 return rootNode; 4236 }; 4237 4238 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 4239 // make it possible to place the caret inside empty blocks. This logic tries to remove 4240 // these elements and keep br elements that where intended to be there intact 4241 if (settings.remove_trailing_brs) { 4242 self.addNodeFilter('br', function(nodes, name) { 4243 var i, l = nodes.length, node, blockElements = tinymce.extend({}, schema.getBlockElements()), 4244 nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 4245 4246 // Remove brs from body element as well 4247 blockElements.body = 1; 4248 4249 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 4250 for (i = 0; i < l; i++) { 4251 node = nodes[i]; 4252 parent = node.parent; 4253 4254 if (blockElements[node.parent.name] && node === parent.lastChild) { 4255 // Loop all nodes to the left of the current node and check for other BR elements 4256 // excluding bookmarks since they are invisible 4257 prev = node.prev; 4258 while (prev) { 4259 prevName = prev.name; 4260 4261 // Ignore bookmarks 4262 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 4263 // Found a non BR element 4264 if (prevName !== "br") 4265 break; 4266 4267 // Found another br it's a <br><br> structure then don't remove anything 4268 if (prevName === 'br') { 4269 node = null; 4270 break; 4271 } 4272 } 4273 4274 prev = prev.prev; 4275 } 4276 4277 if (node) { 4278 node.remove(); 4279 4280 // Is the parent to be considered empty after we removed the BR 4281 if (parent.isEmpty(nonEmptyElements)) { 4282 elementRule = schema.getElementRule(parent.name); 4283 4284 // Remove or padd the element depending on schema rule 4285 if (elementRule) { 4286 if (elementRule.removeEmpty) 4287 parent.remove(); 4288 else if (elementRule.paddEmpty) 4289 parent.empty().append(new tinymce.html.Node('#text', 3)).value = '\u00a0'; 4290 } 4291 } 4292 } 4293 } else { 4294 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> so they become <p><b><i> </i></b></p> 4295 lastParent = node; 4296 while (parent.firstChild === lastParent && parent.lastChild === lastParent) { 4297 lastParent = parent; 4298 4299 if (blockElements[parent.name]) { 4300 break; 4301 } 4302 4303 parent = parent.parent; 4304 } 4305 4306 if (lastParent === parent) { 4307 textNode = new tinymce.html.Node('#text', 3); 4308 textNode.value = '\u00a0'; 4309 node.replace(textNode); 4310 } 4311 } 4312 } 4313 }); 4314 } 4315 4316 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 4317 if (!settings.allow_html_in_named_anchor) { 4318 self.addAttributeFilter('id,name', function(nodes, name) { 4319 var i = nodes.length, sibling, prevSibling, parent, node; 4320 4321 while (i--) { 4322 node = nodes[i]; 4323 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 4324 parent = node.parent; 4325 4326 // Move children after current node 4327 sibling = node.lastChild; 4328 do { 4329 prevSibling = sibling.prev; 4330 parent.insert(sibling, node); 4331 sibling = prevSibling; 4332 } while (sibling); 4333 } 4334 } 4335 }); 4336 } 4337 } 4338 })(tinymce); 4339 4340 tinymce.html.Writer = function(settings) { 4341 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 4342 4343 settings = settings || {}; 4344 indent = settings.indent; 4345 indentBefore = tinymce.makeMap(settings.indent_before || ''); 4346 indentAfter = tinymce.makeMap(settings.indent_after || ''); 4347 encode = tinymce.html.Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 4348 htmlOutput = settings.element_format == "html"; 4349 4350 return { 4351 start: function(name, attrs, empty) { 4352 var i, l, attr, value; 4353 4354 if (indent && indentBefore[name] && html.length > 0) { 4355 value = html[html.length - 1]; 4356 4357 if (value.length > 0 && value !== '\n') 4358 html.push('\n'); 4359 } 4360 4361 html.push('<', name); 4362 4363 if (attrs) { 4364 for (i = 0, l = attrs.length; i < l; i++) { 4365 attr = attrs[i]; 4366 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 4367 } 4368 } 4369 4370 if (!empty || htmlOutput) 4371 html[html.length] = '>'; 4372 else 4373 html[html.length] = ' />'; 4374 4375 if (empty && indent && indentAfter[name] && html.length > 0) { 4376 value = html[html.length - 1]; 4377 4378 if (value.length > 0 && value !== '\n') 4379 html.push('\n'); 4380 } 4381 }, 4382 4383 end: function(name) { 4384 var value; 4385 4386 /*if (indent && indentBefore[name] && html.length > 0) { 4387 value = html[html.length - 1]; 4388 4389 if (value.length > 0 && value !== '\n') 4390 html.push('\n'); 4391 }*/ 4392 4393 html.push('</', name, '>'); 4394 4395 if (indent && indentAfter[name] && html.length > 0) { 4396 value = html[html.length - 1]; 4397 4398 if (value.length > 0 && value !== '\n') 4399 html.push('\n'); 4400 } 4401 }, 4402 4403 text: function(text, raw) { 4404 if (text.length > 0) 4405 html[html.length] = raw ? text : encode(text); 4406 }, 4407 4408 cdata: function(text) { 4409 html.push('<![CDATA[', text, ']]>'); 4410 }, 4411 4412 comment: function(text) { 4413 html.push('<!--', text, '-->'); 4414 }, 4415 4416 pi: function(name, text) { 4417 if (text) 4418 html.push('<?', name, ' ', text, '?>'); 4419 else 4420 html.push('<?', name, '?>'); 4421 4422 if (indent) 4423 html.push('\n'); 4424 }, 4425 4426 doctype: function(text) { 4427 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 4428 }, 4429 4430 reset: function() { 4431 html.length = 0; 4432 }, 4433 4434 getContent: function() { 4435 return html.join('').replace(/\n$/, ''); 4436 } 4437 }; 4438 }; 4439 4440 (function(tinymce) { 4441 tinymce.html.Serializer = function(settings, schema) { 4442 var self = this, writer = new tinymce.html.Writer(settings); 4443 4444 settings = settings || {}; 4445 settings.validate = "validate" in settings ? settings.validate : true; 4446 4447 self.schema = schema = schema || new tinymce.html.Schema(); 4448 self.writer = writer; 4449 4450 self.serialize = function(node) { 4451 var handlers, validate; 4452 4453 validate = settings.validate; 4454 4455 handlers = { 4456 // #text 4457 3: function(node, raw) { 4458 writer.text(node.value, node.raw); 4459 }, 4460 4461 // #comment 4462 8: function(node) { 4463 writer.comment(node.value); 4464 }, 4465 4466 // Processing instruction 4467 7: function(node) { 4468 writer.pi(node.name, node.value); 4469 }, 4470 4471 // Doctype 4472 10: function(node) { 4473 writer.doctype(node.value); 4474 }, 4475 4476 // CDATA 4477 4: function(node) { 4478 writer.cdata(node.value); 4479 }, 4480 4481 // Document fragment 4482 11: function(node) { 4483 if ((node = node.firstChild)) { 4484 do { 4485 walk(node); 4486 } while (node = node.next); 4487 } 4488 } 4489 }; 4490 4491 writer.reset(); 4492 4493 function walk(node) { 4494 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 4495 4496 if (!handler) { 4497 name = node.name; 4498 isEmpty = node.shortEnded; 4499 attrs = node.attributes; 4500 4501 // Sort attributes 4502 if (validate && attrs && attrs.length > 1) { 4503 sortedAttrs = []; 4504 sortedAttrs.map = {}; 4505 4506 elementRule = schema.getElementRule(node.name); 4507 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 4508 attrName = elementRule.attributesOrder[i]; 4509 4510 if (attrName in attrs.map) { 4511 attrValue = attrs.map[attrName]; 4512 sortedAttrs.map[attrName] = attrValue; 4513 sortedAttrs.push({name: attrName, value: attrValue}); 4514 } 4515 } 4516 4517 for (i = 0, l = attrs.length; i < l; i++) { 4518 attrName = attrs[i].name; 4519 4520 if (!(attrName in sortedAttrs.map)) { 4521 attrValue = attrs.map[attrName]; 4522 sortedAttrs.map[attrName] = attrValue; 4523 sortedAttrs.push({name: attrName, value: attrValue}); 4524 } 4525 } 4526 4527 attrs = sortedAttrs; 4528 } 4529 4530 writer.start(node.name, attrs, isEmpty); 4531 4532 if (!isEmpty) { 4533 if ((node = node.firstChild)) { 4534 do { 4535 walk(node); 4536 } while (node = node.next); 4537 } 4538 4539 writer.end(name); 4540 } 4541 } else 4542 handler(node); 4543 } 4544 4545 // Serialize element and treat all non elements as fragments 4546 if (node.type == 1 && !settings.inner) 4547 walk(node); 4548 else 4549 handlers[11](node); 4550 4551 return writer.getContent(); 4552 }; 4553 } 4554 })(tinymce); 4555 4556 // JSLint defined globals 4557 /*global tinymce:false, window:false */ 4558 4559 tinymce.dom = {}; 4560 4561 (function(namespace, expando) { 4562 var w3cEventModel = !!document.addEventListener; 4563 4564 function addEvent(target, name, callback, capture) { 4565 if (target.addEventListener) { 4566 target.addEventListener(name, callback, capture || false); 4567 } else if (target.attachEvent) { 4568 target.attachEvent('on' + name, callback); 4569 } 4570 } 4571 4572 function removeEvent(target, name, callback, capture) { 4573 if (target.removeEventListener) { 4574 target.removeEventListener(name, callback, capture || false); 4575 } else if (target.detachEvent) { 4576 target.detachEvent('on' + name, callback); 4577 } 4578 } 4579 4580 function fix(original_event, data) { 4581 var name, event = data || {}; 4582 4583 // Dummy function that gets replaced on the delegation state functions 4584 function returnFalse() { 4585 return false; 4586 } 4587 4588 // Dummy function that gets replaced on the delegation state functions 4589 function returnTrue() { 4590 return true; 4591 } 4592 4593 // Copy all properties from the original event 4594 for (name in original_event) { 4595 // layerX/layerY is deprecated in Chrome and produces a warning 4596 if (name !== "layerX" && name !== "layerY") { 4597 event[name] = original_event[name]; 4598 } 4599 } 4600 4601 // Normalize target IE uses srcElement 4602 if (!event.target) { 4603 event.target = event.srcElement || document; 4604 } 4605 4606 // Add preventDefault method 4607 event.preventDefault = function() { 4608 event.isDefaultPrevented = returnTrue; 4609 4610 // Execute preventDefault on the original event object 4611 if (original_event) { 4612 if (original_event.preventDefault) { 4613 original_event.preventDefault(); 4614 } else { 4615 original_event.returnValue = false; // IE 4616 } 4617 } 4618 }; 4619 4620 // Add stopPropagation 4621 event.stopPropagation = function() { 4622 event.isPropagationStopped = returnTrue; 4623 4624 // Execute stopPropagation on the original event object 4625 if (original_event) { 4626 if (original_event.stopPropagation) { 4627 original_event.stopPropagation(); 4628 } else { 4629 original_event.cancelBubble = true; // IE 4630 } 4631 } 4632 }; 4633 4634 // Add stopImmediatePropagation 4635 event.stopImmediatePropagation = function() { 4636 event.isImmediatePropagationStopped = returnTrue; 4637 event.stopPropagation(); 4638 }; 4639 4640 // Add event delegation states 4641 if (!event.isDefaultPrevented) { 4642 event.isDefaultPrevented = returnFalse; 4643 event.isPropagationStopped = returnFalse; 4644 event.isImmediatePropagationStopped = returnFalse; 4645 } 4646 4647 return event; 4648 } 4649 4650 function bindOnReady(win, callback, event_utils) { 4651 var doc = win.document, event = {type: 'ready'}; 4652 4653 // Gets called when the DOM is ready 4654 function readyHandler() { 4655 if (!event_utils.domLoaded) { 4656 event_utils.domLoaded = true; 4657 callback(event); 4658 } 4659 } 4660 4661 // Use W3C method 4662 if (w3cEventModel) { 4663 addEvent(win, 'DOMContentLoaded', readyHandler); 4664 } else { 4665 // Use IE method 4666 addEvent(doc, "readystatechange", function() { 4667 if (doc.readyState === "complete") { 4668 removeEvent(doc, "readystatechange", arguments.callee); 4669 readyHandler(); 4670 } 4671 }); 4672 4673 // Wait until we can scroll, when we can the DOM is initialized 4674 if (doc.documentElement.doScroll && win === win.top) { 4675 (function() { 4676 try { 4677 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 4678 // http://javascript.nwbox.com/IEContentLoaded/ 4679 doc.documentElement.doScroll("left"); 4680 } catch (ex) { 4681 setTimeout(arguments.callee, 0); 4682 return; 4683 } 4684 4685 readyHandler(); 4686 })(); 4687 } 4688 } 4689 4690 // Fallback if any of the above methods should fail for some odd reason 4691 addEvent(win, 'load', readyHandler); 4692 } 4693 4694 function EventUtils(proxy) { 4695 var self = this, events = {}, count, isFocusBlurBound, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 4696 4697 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 4698 hasFocusIn = "onfocusin" in document.documentElement; 4699 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 4700 count = 1; 4701 4702 // State if the DOMContentLoaded was executed or not 4703 self.domLoaded = false; 4704 self.events = events; 4705 4706 function executeHandlers(evt, id) { 4707 var callbackList, i, l, callback; 4708 4709 callbackList = events[id][evt.type]; 4710 if (callbackList) { 4711 for (i = 0, l = callbackList.length; i < l; i++) { 4712 callback = callbackList[i]; 4713 4714 // Check if callback exists might be removed if a unbind is called inside the callback 4715 if (callback && callback.func.call(callback.scope, evt) === false) { 4716 evt.preventDefault(); 4717 } 4718 4719 // Should we stop propagation to immediate listeners 4720 if (evt.isImmediatePropagationStopped()) { 4721 return; 4722 } 4723 } 4724 } 4725 } 4726 4727 self.bind = function(target, names, callback, scope) { 4728 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 4729 4730 // Native event handler function patches the event and executes the callbacks for the expando 4731 function defaultNativeHandler(evt) { 4732 executeHandlers(fix(evt || win.event), id); 4733 } 4734 4735 // Don't bind to text nodes or comments 4736 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4737 return; 4738 } 4739 4740 // Create or get events id for the target 4741 if (!target[expando]) { 4742 id = count++; 4743 target[expando] = id; 4744 events[id] = {}; 4745 } else { 4746 id = target[expando]; 4747 4748 if (!events[id]) { 4749 events[id] = {}; 4750 } 4751 } 4752 4753 // Setup the specified scope or use the target as a default 4754 scope = scope || target; 4755 4756 // Split names and bind each event, enables you to bind multiple events with one call 4757 names = names.split(' '); 4758 i = names.length; 4759 while (i--) { 4760 name = names[i]; 4761 nativeHandler = defaultNativeHandler; 4762 fakeName = capture = false; 4763 4764 // Use ready instead of DOMContentLoaded 4765 if (name === "DOMContentLoaded") { 4766 name = "ready"; 4767 } 4768 4769 // DOM is already ready 4770 if ((self.domLoaded || target.readyState == 'complete') && name === "ready") { 4771 self.domLoaded = true; 4772 callback.call(scope, fix({type: name})); 4773 continue; 4774 } 4775 4776 // Handle mouseenter/mouseleaver 4777 if (!hasMouseEnterLeave) { 4778 fakeName = mouseEnterLeave[name]; 4779 4780 if (fakeName) { 4781 nativeHandler = function(evt) { 4782 var current, related; 4783 4784 current = evt.currentTarget; 4785 related = evt.relatedTarget; 4786 4787 // Check if related is inside the current target if it's not then the event should be ignored since it's a mouseover/mouseout inside the element 4788 if (related && current.contains) { 4789 // Use contains for performance 4790 related = current.contains(related); 4791 } else { 4792 while (related && related !== current) { 4793 related = related.parentNode; 4794 } 4795 } 4796 4797 // Fire fake event 4798 if (!related) { 4799 evt = fix(evt || win.event); 4800 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 4801 evt.target = current; 4802 executeHandlers(evt, id); 4803 } 4804 }; 4805 } 4806 } 4807 4808 // Fake bubbeling of focusin/focusout 4809 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 4810 capture = true; 4811 fakeName = name === "focusin" ? "focus" : "blur"; 4812 nativeHandler = function(evt) { 4813 evt = fix(evt || win.event); 4814 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 4815 executeHandlers(evt, id); 4816 }; 4817 } 4818 4819 // Setup callback list and bind native event 4820 callbackList = events[id][name]; 4821 if (!callbackList) { 4822 events[id][name] = callbackList = [{func: callback, scope: scope}]; 4823 callbackList.fakeName = fakeName; 4824 callbackList.capture = capture; 4825 4826 // Add the nativeHandler to the callback list so that we can later unbind it 4827 callbackList.nativeHandler = nativeHandler; 4828 if (!w3cEventModel) { 4829 callbackList.proxyHandler = proxy(id); 4830 } 4831 4832 // Check if the target has native events support 4833 if (name === "ready") { 4834 bindOnReady(target, nativeHandler, self); 4835 } else { 4836 addEvent(target, fakeName || name, w3cEventModel ? nativeHandler : callbackList.proxyHandler, capture); 4837 } 4838 } else { 4839 // If it already has an native handler then just push the callback 4840 callbackList.push({func: callback, scope: scope}); 4841 } 4842 } 4843 4844 target = callbackList = 0; // Clean memory for IE 4845 4846 return callback; 4847 }; 4848 4849 self.unbind = function(target, names, callback) { 4850 var id, callbackList, i, ci, name, eventMap; 4851 4852 // Don't bind to text nodes or comments 4853 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4854 return self; 4855 } 4856 4857 // Unbind event or events if the target has the expando 4858 id = target[expando]; 4859 if (id) { 4860 eventMap = events[id]; 4861 4862 // Specific callback 4863 if (names) { 4864 names = names.split(' '); 4865 i = names.length; 4866 while (i--) { 4867 name = names[i]; 4868 callbackList = eventMap[name]; 4869 4870 // Unbind the event if it exists in the map 4871 if (callbackList) { 4872 // Remove specified callback 4873 if (callback) { 4874 ci = callbackList.length; 4875 while (ci--) { 4876 if (callbackList[ci].func === callback) { 4877 callbackList.splice(ci, 1); 4878 } 4879 } 4880 } 4881 4882 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 4883 if (!callback || callbackList.length === 0) { 4884 delete eventMap[name]; 4885 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4886 } 4887 } 4888 } 4889 } else { 4890 // All events for a specific element 4891 for (name in eventMap) { 4892 callbackList = eventMap[name]; 4893 removeEvent(target, callbackList.fakeName || name, w3cEventModel ? callbackList.nativeHandler : callbackList.proxyHandler, callbackList.capture); 4894 } 4895 4896 eventMap = {}; 4897 } 4898 4899 // Check if object is empty, if it isn't then we won't remove the expando map 4900 for (name in eventMap) { 4901 return self; 4902 } 4903 4904 // Delete event object 4905 delete events[id]; 4906 4907 // Remove expando from target 4908 try { 4909 // IE will fail here since it can't delete properties from window 4910 delete target[expando]; 4911 } catch (ex) { 4912 // IE will set it to null 4913 target[expando] = null; 4914 } 4915 } 4916 4917 return self; 4918 }; 4919 4920 self.fire = function(target, name, args) { 4921 var id, event; 4922 4923 // Don't bind to text nodes or comments 4924 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4925 return self; 4926 } 4927 4928 // Build event object by patching the args 4929 event = fix(null, args); 4930 event.type = name; 4931 4932 do { 4933 // Found an expando that means there is listeners to execute 4934 id = target[expando]; 4935 if (id) { 4936 executeHandlers(event, id); 4937 } 4938 4939 // Walk up the DOM 4940 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 4941 } while (target && !event.isPropagationStopped()); 4942 4943 return self; 4944 }; 4945 4946 self.clean = function(target) { 4947 var i, children, unbind = self.unbind; 4948 4949 // Don't bind to text nodes or comments 4950 if (!target || target.nodeType === 3 || target.nodeType === 8) { 4951 return self; 4952 } 4953 4954 // Unbind any element on the specificed target 4955 if (target[expando]) { 4956 unbind(target); 4957 } 4958 4959 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 4960 if (!target.getElementsByTagName) { 4961 target = target.document; 4962 } 4963 4964 // Remove events from each child element 4965 if (target && target.getElementsByTagName) { 4966 unbind(target); 4967 4968 children = target.getElementsByTagName('*'); 4969 i = children.length; 4970 while (i--) { 4971 target = children[i]; 4972 4973 if (target[expando]) { 4974 unbind(target); 4975 } 4976 } 4977 } 4978 4979 return self; 4980 }; 4981 4982 self.callNativeHandler = function(id, evt) { 4983 if (events) { 4984 events[id][evt.type].nativeHandler(evt); 4985 } 4986 }; 4987 4988 self.destory = function() { 4989 events = {}; 4990 }; 4991 4992 // Legacy function calls 4993 4994 self.add = function(target, events, func, scope) { 4995 // Old API supported direct ID assignment 4996 if (typeof(target) === "string") { 4997 target = document.getElementById(target); 4998 } 4999 5000 // Old API supported multiple targets 5001 if (target && target instanceof Array) { 5002 var i = target.length; 5003 5004 while (i--) { 5005 self.add(target[i], events, func, scope); 5006 } 5007 5008 return; 5009 } 5010 5011 // Old API called ready init 5012 if (events === "init") { 5013 events = "ready"; 5014 } 5015 5016 return self.bind(target, events instanceof Array ? events.join(' ') : events, func, scope); 5017 }; 5018 5019 self.remove = function(target, events, func, scope) { 5020 if (!target) { 5021 return self; 5022 } 5023 5024 // Old API supported direct ID assignment 5025 if (typeof(target) === "string") { 5026 target = document.getElementById(target); 5027 } 5028 5029 // Old API supported multiple targets 5030 if (target instanceof Array) { 5031 var i = target.length; 5032 5033 while (i--) { 5034 self.remove(target[i], events, func, scope); 5035 } 5036 5037 return self; 5038 } 5039 5040 return self.unbind(target, events instanceof Array ? events.join(' ') : events, func); 5041 }; 5042 5043 self.clear = function(target) { 5044 // Old API supported direct ID assignment 5045 if (typeof(target) === "string") { 5046 target = document.getElementById(target); 5047 } 5048 5049 return self.clean(target); 5050 }; 5051 5052 self.cancel = function(e) { 5053 if (e) { 5054 self.prevent(e); 5055 self.stop(e); 5056 } 5057 5058 return false; 5059 }; 5060 5061 self.prevent = function(e) { 5062 if (!e.preventDefault) { 5063 e = fix(e); 5064 } 5065 5066 e.preventDefault(); 5067 5068 return false; 5069 }; 5070 5071 self.stop = function(e) { 5072 if (!e.stopPropagation) { 5073 e = fix(e); 5074 } 5075 5076 e.stopPropagation(); 5077 5078 return false; 5079 }; 5080 } 5081 5082 namespace.EventUtils = EventUtils; 5083 5084 namespace.Event = new EventUtils(function(id) { 5085 return function(evt) { 5086 tinymce.dom.Event.callNativeHandler(id, evt); 5087 }; 5088 }); 5089 5090 // Bind ready event when tinymce script is loaded 5091 namespace.Event.bind(window, 'ready', function() {}); 5092 5093 namespace = 0; 5094 })(tinymce.dom, 'data-mce-expando'); // Namespace and expando 5095 5096 tinymce.dom.TreeWalker = function(start_node, root_node) { 5097 var node = start_node; 5098 5099 function findSibling(node, start_name, sibling_name, shallow) { 5100 var sibling, parent; 5101 5102 if (node) { 5103 // Walk into nodes if it has a start 5104 if (!shallow && node[start_name]) 5105 return node[start_name]; 5106 5107 // Return the sibling if it has one 5108 if (node != root_node) { 5109 sibling = node[sibling_name]; 5110 if (sibling) 5111 return sibling; 5112 5113 // Walk up the parents to look for siblings 5114 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { 5115 sibling = parent[sibling_name]; 5116 if (sibling) 5117 return sibling; 5118 } 5119 } 5120 } 5121 }; 5122 5123 this.current = function() { 5124 return node; 5125 }; 5126 5127 this.next = function(shallow) { 5128 return (node = findSibling(node, 'firstChild', 'nextSibling', shallow)); 5129 }; 5130 5131 this.prev = function(shallow) { 5132 return (node = findSibling(node, 'lastChild', 'previousSibling', shallow)); 5133 }; 5134 }; 5135 5136 (function(tinymce) { 5137 // Shorten names 5138 var each = tinymce.each, 5139 is = tinymce.is, 5140 isWebKit = tinymce.isWebKit, 5141 isIE = tinymce.isIE, 5142 Entities = tinymce.html.Entities, 5143 simpleSelectorRe = /^([a-z0-9],?)+$/i, 5144 whiteSpaceRegExp = /^[ \t\r\n]*$/; 5145 5146 tinymce.create('tinymce.dom.DOMUtils', { 5147 doc : null, 5148 root : null, 5149 files : null, 5150 pixelStyles : /^(top|left|bottom|right|width|height|borderWidth)$/, 5151 props : { 5152 "for" : "htmlFor", 5153 "class" : "className", 5154 className : "className", 5155 checked : "checked", 5156 disabled : "disabled", 5157 maxlength : "maxLength", 5158 readonly : "readOnly", 5159 selected : "selected", 5160 value : "value", 5161 id : "id", 5162 name : "name", 5163 type : "type" 5164 }, 5165 5166 DOMUtils : function(d, s) { 5167 var t = this, globalStyle, name, blockElementsMap; 5168 5169 t.doc = d; 5170 t.win = window; 5171 t.files = {}; 5172 t.cssFlicker = false; 5173 t.counter = 0; 5174 t.stdMode = !tinymce.isIE || d.documentMode >= 8; 5175 t.boxModel = !tinymce.isIE || d.compatMode == "CSS1Compat" || t.stdMode; 5176 t.hasOuterHTML = "outerHTML" in d.createElement("a"); 5177 5178 t.settings = s = tinymce.extend({ 5179 keep_values : false, 5180 hex_colors : 1 5181 }, s); 5182 5183 t.schema = s.schema; 5184 t.styles = new tinymce.html.Styles({ 5185 url_converter : s.url_converter, 5186 url_converter_scope : s.url_converter_scope 5187 }, s.schema); 5188 5189 // Fix IE6SP2 flicker and check it failed for pre SP2 5190 if (tinymce.isIE6) { 5191 try { 5192 d.execCommand('BackgroundImageCache', false, true); 5193 } catch (e) { 5194 t.cssFlicker = true; 5195 } 5196 } 5197 5198 t.fixDoc(d); 5199 t.events = s.ownEvents ? new tinymce.dom.EventUtils(s.proxy) : tinymce.dom.Event; 5200 tinymce.addUnload(t.destroy, t); 5201 blockElementsMap = s.schema ? s.schema.getBlockElements() : {}; 5202 5203 t.isBlock = function(node) { 5204 // This function is called in module pattern style since it might be executed with the wrong this scope 5205 var type = node.nodeType; 5206 5207 // If it's a node then check the type and use the nodeName 5208 if (type) 5209 return !!(type === 1 && blockElementsMap[node.nodeName]); 5210 5211 return !!blockElementsMap[node]; 5212 }; 5213 }, 5214 5215 fixDoc: function(doc) { 5216 var settings = this.settings, name; 5217 5218 if (isIE && settings.schema) { 5219 // Add missing HTML 4/5 elements to IE 5220 ('abbr article aside audio canvas ' + 5221 'details figcaption figure footer ' + 5222 'header hgroup mark menu meter nav ' + 5223 'output progress section summary ' + 5224 'time video').replace(/\w+/g, function(name) { 5225 doc.createElement(name); 5226 }); 5227 5228 // Create all custom elements 5229 for (name in settings.schema.getCustomElements()) { 5230 doc.createElement(name); 5231 } 5232 } 5233 }, 5234 5235 clone: function(node, deep) { 5236 var self = this, clone, doc; 5237 5238 // TODO: Add feature detection here in the future 5239 if (!isIE || node.nodeType !== 1 || deep) { 5240 return node.cloneNode(deep); 5241 } 5242 5243 doc = self.doc; 5244 5245 // Make a HTML5 safe shallow copy 5246 if (!deep) { 5247 clone = doc.createElement(node.nodeName); 5248 5249 // Copy attribs 5250 each(self.getAttribs(node), function(attr) { 5251 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 5252 }); 5253 5254 return clone; 5255 } 5256 /* 5257 // Setup HTML5 patched document fragment 5258 if (!self.frag) { 5259 self.frag = doc.createDocumentFragment(); 5260 self.fixDoc(self.frag); 5261 } 5262 5263 // Make a deep copy by adding it to the document fragment then removing it this removed the :section 5264 clone = doc.createElement('div'); 5265 self.frag.appendChild(clone); 5266 clone.innerHTML = node.outerHTML; 5267 self.frag.removeChild(clone); 5268 */ 5269 return clone.firstChild; 5270 }, 5271 5272 getRoot : function() { 5273 var t = this, s = t.settings; 5274 5275 return (s && t.get(s.root_element)) || t.doc.body; 5276 }, 5277 5278 getViewPort : function(w) { 5279 var d, b; 5280 5281 w = !w ? this.win : w; 5282 d = w.document; 5283 b = this.boxModel ? d.documentElement : d.body; 5284 5285 // Returns viewport size excluding scrollbars 5286 return { 5287 x : w.pageXOffset || b.scrollLeft, 5288 y : w.pageYOffset || b.scrollTop, 5289 w : w.innerWidth || b.clientWidth, 5290 h : w.innerHeight || b.clientHeight 5291 }; 5292 }, 5293 5294 getRect : function(e) { 5295 var p, t = this, sr; 5296 5297 e = t.get(e); 5298 p = t.getPos(e); 5299 sr = t.getSize(e); 5300 5301 return { 5302 x : p.x, 5303 y : p.y, 5304 w : sr.w, 5305 h : sr.h 5306 }; 5307 }, 5308 5309 getSize : function(e) { 5310 var t = this, w, h; 5311 5312 e = t.get(e); 5313 w = t.getStyle(e, 'width'); 5314 h = t.getStyle(e, 'height'); 5315 5316 // Non pixel value, then force offset/clientWidth 5317 if (w.indexOf('px') === -1) 5318 w = 0; 5319 5320 // Non pixel value, then force offset/clientWidth 5321 if (h.indexOf('px') === -1) 5322 h = 0; 5323 5324 return { 5325 w : parseInt(w, 10) || e.offsetWidth || e.clientWidth, 5326 h : parseInt(h, 10) || e.offsetHeight || e.clientHeight 5327 }; 5328 }, 5329 5330 getParent : function(n, f, r) { 5331 return this.getParents(n, f, r, false); 5332 }, 5333 5334 getParents : function(n, f, r, c) { 5335 var t = this, na, se = t.settings, o = []; 5336 5337 n = t.get(n); 5338 c = c === undefined; 5339 5340 if (se.strict_root) 5341 r = r || t.getRoot(); 5342 5343 // Wrap node name as func 5344 if (is(f, 'string')) { 5345 na = f; 5346 5347 if (f === '*') { 5348 f = function(n) {return n.nodeType == 1;}; 5349 } else { 5350 f = function(n) { 5351 return t.is(n, na); 5352 }; 5353 } 5354 } 5355 5356 while (n) { 5357 if (n == r || !n.nodeType || n.nodeType === 9) 5358 break; 5359 5360 if (!f || f(n)) { 5361 if (c) 5362 o.push(n); 5363 else 5364 return n; 5365 } 5366 5367 n = n.parentNode; 5368 } 5369 5370 return c ? o : null; 5371 }, 5372 5373 get : function(e) { 5374 var n; 5375 5376 if (e && this.doc && typeof(e) == 'string') { 5377 n = e; 5378 e = this.doc.getElementById(e); 5379 5380 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 5381 if (e && e.id !== n) 5382 return this.doc.getElementsByName(n)[1]; 5383 } 5384 5385 return e; 5386 }, 5387 5388 getNext : function(node, selector) { 5389 return this._findSib(node, selector, 'nextSibling'); 5390 }, 5391 5392 getPrev : function(node, selector) { 5393 return this._findSib(node, selector, 'previousSibling'); 5394 }, 5395 5396 5397 add : function(p, n, a, h, c) { 5398 var t = this; 5399 5400 return this.run(p, function(p) { 5401 var e, k; 5402 5403 e = is(n, 'string') ? t.doc.createElement(n) : n; 5404 t.setAttribs(e, a); 5405 5406 if (h) { 5407 if (h.nodeType) 5408 e.appendChild(h); 5409 else 5410 t.setHTML(e, h); 5411 } 5412 5413 return !c ? p.appendChild(e) : e; 5414 }); 5415 }, 5416 5417 create : function(n, a, h) { 5418 return this.add(this.doc.createElement(n), n, a, h, 1); 5419 }, 5420 5421 createHTML : function(n, a, h) { 5422 var o = '', t = this, k; 5423 5424 o += '<' + n; 5425 5426 for (k in a) { 5427 if (a.hasOwnProperty(k)) 5428 o += ' ' + k + '="' + t.encode(a[k]) + '"'; 5429 } 5430 5431 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 5432 if (typeof(h) != "undefined") 5433 return o + '>' + h + '</' + n + '>'; 5434 5435 return o + ' />'; 5436 }, 5437 5438 remove : function(node, keep_children) { 5439 return this.run(node, function(node) { 5440 var child, parent = node.parentNode; 5441 5442 if (!parent) 5443 return null; 5444 5445 if (keep_children) { 5446 while (child = node.firstChild) { 5447 // IE 8 will crash if you don't remove completely empty text nodes 5448 if (!tinymce.isIE || child.nodeType !== 3 || child.nodeValue) 5449 parent.insertBefore(child, node); 5450 else 5451 node.removeChild(child); 5452 } 5453 } 5454 5455 return parent.removeChild(node); 5456 }); 5457 }, 5458 5459 setStyle : function(n, na, v) { 5460 var t = this; 5461 5462 return t.run(n, function(e) { 5463 var s, i; 5464 5465 s = e.style; 5466 5467 // Camelcase it, if needed 5468 na = na.replace(/-(\D)/g, function(a, b){ 5469 return b.toUpperCase(); 5470 }); 5471 5472 // Default px suffix on these 5473 if (t.pixelStyles.test(na) && (tinymce.is(v, 'number') || /^[\-0-9\.]+$/.test(v))) 5474 v += 'px'; 5475 5476 switch (na) { 5477 case 'opacity': 5478 // IE specific opacity 5479 if (isIE) { 5480 s.filter = v === '' ? '' : "alpha(opacity=" + (v * 100) + ")"; 5481 5482 if (!n.currentStyle || !n.currentStyle.hasLayout) 5483 s.display = 'inline-block'; 5484 } 5485 5486 // Fix for older browsers 5487 s[na] = s['-moz-opacity'] = s['-khtml-opacity'] = v || ''; 5488 break; 5489 5490 case 'float': 5491 isIE ? s.styleFloat = v : s.cssFloat = v; 5492 break; 5493 5494 default: 5495 s[na] = v || ''; 5496 } 5497 5498 // Force update of the style data 5499 if (t.settings.update_styles) 5500 t.setAttrib(e, 'data-mce-style'); 5501 }); 5502 }, 5503 5504 getStyle : function(n, na, c) { 5505 n = this.get(n); 5506 5507 if (!n) 5508 return; 5509 5510 // Gecko 5511 if (this.doc.defaultView && c) { 5512 // Remove camelcase 5513 na = na.replace(/[A-Z]/g, function(a){ 5514 return '-' + a; 5515 }); 5516 5517 try { 5518 return this.doc.defaultView.getComputedStyle(n, null).getPropertyValue(na); 5519 } catch (ex) { 5520 // Old safari might fail 5521 return null; 5522 } 5523 } 5524 5525 // Camelcase it, if needed 5526 na = na.replace(/-(\D)/g, function(a, b){ 5527 return b.toUpperCase(); 5528 }); 5529 5530 if (na == 'float') 5531 na = isIE ? 'styleFloat' : 'cssFloat'; 5532 5533 // IE & Opera 5534 if (n.currentStyle && c) 5535 return n.currentStyle[na]; 5536 5537 return n.style ? n.style[na] : undefined; 5538 }, 5539 5540 setStyles : function(e, o) { 5541 var t = this, s = t.settings, ol; 5542 5543 ol = s.update_styles; 5544 s.update_styles = 0; 5545 5546 each(o, function(v, n) { 5547 t.setStyle(e, n, v); 5548 }); 5549 5550 // Update style info 5551 s.update_styles = ol; 5552 if (s.update_styles) 5553 t.setAttrib(e, s.cssText); 5554 }, 5555 5556 removeAllAttribs: function(e) { 5557 return this.run(e, function(e) { 5558 var i, attrs = e.attributes; 5559 for (i = attrs.length - 1; i >= 0; i--) { 5560 e.removeAttributeNode(attrs.item(i)); 5561 } 5562 }); 5563 }, 5564 5565 setAttrib : function(e, n, v) { 5566 var t = this; 5567 5568 // Whats the point 5569 if (!e || !n) 5570 return; 5571 5572 // Strict XML mode 5573 if (t.settings.strict) 5574 n = n.toLowerCase(); 5575 5576 return this.run(e, function(e) { 5577 var s = t.settings; 5578 var originalValue = e.getAttribute(n); 5579 if (v !== null) { 5580 switch (n) { 5581 case "style": 5582 if (!is(v, 'string')) { 5583 each(v, function(v, n) { 5584 t.setStyle(e, n, v); 5585 }); 5586 5587 return; 5588 } 5589 5590 // No mce_style for elements with these since they might get resized by the user 5591 if (s.keep_values) { 5592 if (v && !t._isRes(v)) 5593 e.setAttribute('data-mce-style', v, 2); 5594 else 5595 e.removeAttribute('data-mce-style', 2); 5596 } 5597 5598 e.style.cssText = v; 5599 break; 5600 5601 case "class": 5602 e.className = v || ''; // Fix IE null bug 5603 break; 5604 5605 case "src": 5606 case "href": 5607 if (s.keep_values) { 5608 if (s.url_converter) 5609 v = s.url_converter.call(s.url_converter_scope || t, v, n, e); 5610 5611 t.setAttrib(e, 'data-mce-' + n, v, 2); 5612 } 5613 5614 break; 5615 5616 case "shape": 5617 e.setAttribute('data-mce-style', v); 5618 break; 5619 } 5620 } 5621 if (is(v) && v !== null && v.length !== 0) 5622 e.setAttribute(n, '' + v, 2); 5623 else 5624 e.removeAttribute(n, 2); 5625 5626 // fire onChangeAttrib event for attributes that have changed 5627 if (tinyMCE.activeEditor && originalValue != v) { 5628 var ed = tinyMCE.activeEditor; 5629 ed.onSetAttrib.dispatch(ed, e, n, v); 5630 } 5631 }); 5632 }, 5633 5634 setAttribs : function(e, o) { 5635 var t = this; 5636 5637 return this.run(e, function(e) { 5638 each(o, function(v, n) { 5639 t.setAttrib(e, n, v); 5640 }); 5641 }); 5642 }, 5643 5644 getAttrib : function(e, n, dv) { 5645 var v, t = this, undef; 5646 5647 e = t.get(e); 5648 5649 if (!e || e.nodeType !== 1) 5650 return dv === undef ? false : dv; 5651 5652 if (!is(dv)) 5653 dv = ''; 5654 5655 // Try the mce variant for these 5656 if (/^(src|href|style|coords|shape)$/.test(n)) { 5657 v = e.getAttribute("data-mce-" + n); 5658 5659 if (v) 5660 return v; 5661 } 5662 5663 if (isIE && t.props[n]) { 5664 v = e[t.props[n]]; 5665 v = v && v.nodeValue ? v.nodeValue : v; 5666 } 5667 5668 if (!v) 5669 v = e.getAttribute(n, 2); 5670 5671 // Check boolean attribs 5672 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(n)) { 5673 if (e[t.props[n]] === true && v === '') 5674 return n; 5675 5676 return v ? n : ''; 5677 } 5678 5679 // Inner input elements will override attributes on form elements 5680 if (e.nodeName === "FORM" && e.getAttributeNode(n)) 5681 return e.getAttributeNode(n).nodeValue; 5682 5683 if (n === 'style') { 5684 v = v || e.style.cssText; 5685 5686 if (v) { 5687 v = t.serializeStyle(t.parseStyle(v), e.nodeName); 5688 5689 if (t.settings.keep_values && !t._isRes(v)) 5690 e.setAttribute('data-mce-style', v); 5691 } 5692 } 5693 5694 // Remove Apple and WebKit stuff 5695 if (isWebKit && n === "class" && v) 5696 v = v.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); 5697 5698 // Handle IE issues 5699 if (isIE) { 5700 switch (n) { 5701 case 'rowspan': 5702 case 'colspan': 5703 // IE returns 1 as default value 5704 if (v === 1) 5705 v = ''; 5706 5707 break; 5708 5709 case 'size': 5710 // IE returns +0 as default value for size 5711 if (v === '+0' || v === 20 || v === 0) 5712 v = ''; 5713 5714 break; 5715 5716 case 'width': 5717 case 'height': 5718 case 'vspace': 5719 case 'checked': 5720 case 'disabled': 5721 case 'readonly': 5722 if (v === 0) 5723 v = ''; 5724 5725 break; 5726 5727 case 'hspace': 5728 // IE returns -1 as default value 5729 if (v === -1) 5730 v = ''; 5731 5732 break; 5733 5734 case 'maxlength': 5735 case 'tabindex': 5736 // IE returns default value 5737 if (v === 32768 || v === 2147483647 || v === '32768') 5738 v = ''; 5739 5740 break; 5741 5742 case 'multiple': 5743 case 'compact': 5744 case 'noshade': 5745 case 'nowrap': 5746 if (v === 65535) 5747 return n; 5748 5749 return dv; 5750 5751 case 'shape': 5752 v = v.toLowerCase(); 5753 break; 5754 5755 default: 5756 // IE has odd anonymous function for event attributes 5757 if (n.indexOf('on') === 0 && v) 5758 v = tinymce._replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1', '' + v); 5759 } 5760 } 5761 5762 return (v !== undef && v !== null && v !== '') ? '' + v : dv; 5763 }, 5764 5765 getPos : function(n, ro) { 5766 var t = this, x = 0, y = 0, e, d = t.doc, r; 5767 5768 n = t.get(n); 5769 ro = ro || d.body; 5770 5771 if (n) { 5772 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 5773 if (n.getBoundingClientRect) { 5774 n = n.getBoundingClientRect(); 5775 e = t.boxModel ? d.documentElement : d.body; 5776 5777 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 5778 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 5779 x = n.left + (d.documentElement.scrollLeft || d.body.scrollLeft) - e.clientTop; 5780 y = n.top + (d.documentElement.scrollTop || d.body.scrollTop) - e.clientLeft; 5781 5782 return {x : x, y : y}; 5783 } 5784 5785 r = n; 5786 while (r && r != ro && r.nodeType) { 5787 x += r.offsetLeft || 0; 5788 y += r.offsetTop || 0; 5789 r = r.offsetParent; 5790 } 5791 5792 r = n.parentNode; 5793 while (r && r != ro && r.nodeType) { 5794 x -= r.scrollLeft || 0; 5795 y -= r.scrollTop || 0; 5796 r = r.parentNode; 5797 } 5798 } 5799 5800 return {x : x, y : y}; 5801 }, 5802 5803 parseStyle : function(st) { 5804 return this.styles.parse(st); 5805 }, 5806 5807 serializeStyle : function(o, name) { 5808 return this.styles.serialize(o, name); 5809 }, 5810 5811 addStyle: function(cssText) { 5812 var doc = this.doc, head; 5813 5814 // Create style element if needed 5815 styleElm = doc.getElementById('mceDefaultStyles'); 5816 if (!styleElm) { 5817 styleElm = doc.createElement('style'), 5818 styleElm.id = 'mceDefaultStyles'; 5819 styleElm.type = 'text/css'; 5820 5821 head = doc.getElementsByTagName('head')[0] 5822 if (head.firstChild) { 5823 head.insertBefore(styleElm, head.firstChild); 5824 } else { 5825 head.appendChild(styleElm); 5826 } 5827 } 5828 5829 // Append style data to old or new style element 5830 if (styleElm.styleSheet) { 5831 styleElm.styleSheet.cssText += cssText; 5832 } else { 5833 styleElm.appendChild(doc.createTextNode(cssText)); 5834 } 5835 }, 5836 5837 loadCSS : function(u) { 5838 var t = this, d = t.doc, head; 5839 5840 if (!u) 5841 u = ''; 5842 5843 head = d.getElementsByTagName('head')[0]; 5844 5845 each(u.split(','), function(u) { 5846 var link; 5847 5848 if (t.files[u]) 5849 return; 5850 5851 t.files[u] = true; 5852 link = t.create('link', {rel : 'stylesheet', href : tinymce._addVer(u)}); 5853 5854 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 5855 // This fix seems to resolve that issue by realcing the document ones a stylesheet finishes loading 5856 // It's ugly but it seems to work fine. 5857 if (isIE && d.documentMode && d.recalc) { 5858 link.onload = function() { 5859 if (d.recalc) 5860 d.recalc(); 5861 5862 link.onload = null; 5863 }; 5864 } 5865 5866 head.appendChild(link); 5867 }); 5868 }, 5869 5870 addClass : function(e, c) { 5871 return this.run(e, function(e) { 5872 var o; 5873 5874 if (!c) 5875 return 0; 5876 5877 if (this.hasClass(e, c)) 5878 return e.className; 5879 5880 o = this.removeClass(e, c); 5881 5882 return e.className = (o != '' ? (o + ' ') : '') + c; 5883 }); 5884 }, 5885 5886 removeClass : function(e, c) { 5887 var t = this, re; 5888 5889 return t.run(e, function(e) { 5890 var v; 5891 5892 if (t.hasClass(e, c)) { 5893 if (!re) 5894 re = new RegExp("(^|\\s+)" + c + "(\\s+|$)", "g"); 5895 5896 v = e.className.replace(re, ' '); 5897 v = tinymce.trim(v != ' ' ? v : ''); 5898 5899 e.className = v; 5900 5901 // Empty class attr 5902 if (!v) { 5903 e.removeAttribute('class'); 5904 e.removeAttribute('className'); 5905 } 5906 5907 return v; 5908 } 5909 5910 return e.className; 5911 }); 5912 }, 5913 5914 hasClass : function(n, c) { 5915 n = this.get(n); 5916 5917 if (!n || !c) 5918 return false; 5919 5920 return (' ' + n.className + ' ').indexOf(' ' + c + ' ') !== -1; 5921 }, 5922 5923 show : function(e) { 5924 return this.setStyle(e, 'display', 'block'); 5925 }, 5926 5927 hide : function(e) { 5928 return this.setStyle(e, 'display', 'none'); 5929 }, 5930 5931 isHidden : function(e) { 5932 e = this.get(e); 5933 5934 return !e || e.style.display == 'none' || this.getStyle(e, 'display') == 'none'; 5935 }, 5936 5937 uniqueId : function(p) { 5938 return (!p ? 'mce_' : p) + (this.counter++); 5939 }, 5940 5941 setHTML : function(element, html) { 5942 var self = this; 5943 5944 return self.run(element, function(element) { 5945 if (isIE) { 5946 // Remove all child nodes, IE keeps empty text nodes in DOM 5947 while (element.firstChild) 5948 element.removeChild(element.firstChild); 5949 5950 try { 5951 // IE will remove comments from the beginning 5952 // unless you padd the contents with something 5953 element.innerHTML = '<br />' + html; 5954 element.removeChild(element.firstChild); 5955 } catch (ex) { 5956 // IE sometimes produces an unknown runtime error on innerHTML if it's an block element within a block element for example a div inside a p 5957 // This seems to fix this problem 5958 5959 // Create new div with HTML contents and a BR infront to keep comments 5960 var newElement = self.create('div'); 5961 newElement.innerHTML = '<br />' + html; 5962 5963 // Add all children from div to target 5964 each (tinymce.grep(newElement.childNodes), function(node, i) { 5965 // Skip br element 5966 if (i && element.canHaveHTML) 5967 element.appendChild(node); 5968 }); 5969 } 5970 } else 5971 element.innerHTML = html; 5972 5973 return html; 5974 }); 5975 }, 5976 5977 getOuterHTML : function(elm) { 5978 var doc, self = this; 5979 5980 elm = self.get(elm); 5981 5982 if (!elm) 5983 return null; 5984 5985 if (elm.nodeType === 1 && self.hasOuterHTML) 5986 return elm.outerHTML; 5987 5988 doc = (elm.ownerDocument || self.doc).createElement("body"); 5989 doc.appendChild(elm.cloneNode(true)); 5990 5991 return doc.innerHTML; 5992 }, 5993 5994 setOuterHTML : function(e, h, d) { 5995 var t = this; 5996 5997 function setHTML(e, h, d) { 5998 var n, tp; 5999 6000 tp = d.createElement("body"); 6001 tp.innerHTML = h; 6002 6003 n = tp.lastChild; 6004 while (n) { 6005 t.insertAfter(n.cloneNode(true), e); 6006 n = n.previousSibling; 6007 } 6008 6009 t.remove(e); 6010 }; 6011 6012 return this.run(e, function(e) { 6013 e = t.get(e); 6014 6015 // Only set HTML on elements 6016 if (e.nodeType == 1) { 6017 d = d || e.ownerDocument || t.doc; 6018 6019 if (isIE) { 6020 try { 6021 // Try outerHTML for IE it sometimes produces an unknown runtime error 6022 if (isIE && e.nodeType == 1) 6023 e.outerHTML = h; 6024 else 6025 setHTML(e, h, d); 6026 } catch (ex) { 6027 // Fix for unknown runtime error 6028 setHTML(e, h, d); 6029 } 6030 } else 6031 setHTML(e, h, d); 6032 } 6033 }); 6034 }, 6035 6036 decode : Entities.decode, 6037 6038 encode : Entities.encodeAllRaw, 6039 6040 insertAfter : function(node, reference_node) { 6041 reference_node = this.get(reference_node); 6042 6043 return this.run(node, function(node) { 6044 var parent, nextSibling; 6045 6046 parent = reference_node.parentNode; 6047 nextSibling = reference_node.nextSibling; 6048 6049 if (nextSibling) 6050 parent.insertBefore(node, nextSibling); 6051 else 6052 parent.appendChild(node); 6053 6054 return node; 6055 }); 6056 }, 6057 6058 replace : function(n, o, k) { 6059 var t = this; 6060 6061 if (is(o, 'array')) 6062 n = n.cloneNode(true); 6063 6064 return t.run(o, function(o) { 6065 if (k) { 6066 each(tinymce.grep(o.childNodes), function(c) { 6067 n.appendChild(c); 6068 }); 6069 } 6070 6071 return o.parentNode.replaceChild(n, o); 6072 }); 6073 }, 6074 6075 rename : function(elm, name) { 6076 var t = this, newElm; 6077 6078 if (elm.nodeName != name.toUpperCase()) { 6079 // Rename block element 6080 newElm = t.create(name); 6081 6082 // Copy attribs to new block 6083 each(t.getAttribs(elm), function(attr_node) { 6084 t.setAttrib(newElm, attr_node.nodeName, t.getAttrib(elm, attr_node.nodeName)); 6085 }); 6086 6087 // Replace block 6088 t.replace(newElm, elm, 1); 6089 } 6090 6091 return newElm || elm; 6092 }, 6093 6094 findCommonAncestor : function(a, b) { 6095 var ps = a, pe; 6096 6097 while (ps) { 6098 pe = b; 6099 6100 while (pe && ps != pe) 6101 pe = pe.parentNode; 6102 6103 if (ps == pe) 6104 break; 6105 6106 ps = ps.parentNode; 6107 } 6108 6109 if (!ps && a.ownerDocument) 6110 return a.ownerDocument.documentElement; 6111 6112 return ps; 6113 }, 6114 6115 toHex : function(s) { 6116 var c = /^\s*rgb\s*?\(\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?,\s*?([0-9]+)\s*?\)\s*$/i.exec(s); 6117 6118 function hex(s) { 6119 s = parseInt(s, 10).toString(16); 6120 6121 return s.length > 1 ? s : '0' + s; // 0 -> 00 6122 }; 6123 6124 if (c) { 6125 s = '#' + hex(c[1]) + hex(c[2]) + hex(c[3]); 6126 6127 return s; 6128 } 6129 6130 return s; 6131 }, 6132 6133 getClasses : function() { 6134 var t = this, cl = [], i, lo = {}, f = t.settings.class_filter, ov; 6135 6136 if (t.classes) 6137 return t.classes; 6138 6139 function addClasses(s) { 6140 // IE style imports 6141 each(s.imports, function(r) { 6142 addClasses(r); 6143 }); 6144 6145 each(s.cssRules || s.rules, function(r) { 6146 // Real type or fake it on IE 6147 switch (r.type || 1) { 6148 // Rule 6149 case 1: 6150 if (r.selectorText) { 6151 each(r.selectorText.split(','), function(v) { 6152 v = v.replace(/^\s*|\s*$|^\s\./g, ""); 6153 6154 // Is internal or it doesn't contain a class 6155 if (/\.mce/.test(v) || !/\.[\w\-]+$/.test(v)) 6156 return; 6157 6158 // Remove everything but class name 6159 ov = v; 6160 v = tinymce._replace(/.*\.([a-z0-9_\-]+).*/i, '$1', v); 6161 6162 // Filter classes 6163 if (f && !(v = f(v, ov))) 6164 return; 6165 6166 if (!lo[v]) { 6167 cl.push({'class' : v}); 6168 lo[v] = 1; 6169 } 6170 }); 6171 } 6172 break; 6173 6174 // Import 6175 case 3: 6176 addClasses(r.styleSheet); 6177 break; 6178 } 6179 }); 6180 }; 6181 6182 try { 6183 each(t.doc.styleSheets, addClasses); 6184 } catch (ex) { 6185 // Ignore 6186 } 6187 6188 if (cl.length > 0) 6189 t.classes = cl; 6190 6191 return cl; 6192 }, 6193 6194 run : function(e, f, s) { 6195 var t = this, o; 6196 6197 if (t.doc && typeof(e) === 'string') 6198 e = t.get(e); 6199 6200 if (!e) 6201 return false; 6202 6203 s = s || this; 6204 if (!e.nodeType && (e.length || e.length === 0)) { 6205 o = []; 6206 6207 each(e, function(e, i) { 6208 if (e) { 6209 if (typeof(e) == 'string') 6210 e = t.doc.getElementById(e); 6211 6212 o.push(f.call(s, e, i)); 6213 } 6214 }); 6215 6216 return o; 6217 } 6218 6219 return f.call(s, e); 6220 }, 6221 6222 getAttribs : function(n) { 6223 var o; 6224 6225 n = this.get(n); 6226 6227 if (!n) 6228 return []; 6229 6230 if (isIE) { 6231 o = []; 6232 6233 // Object will throw exception in IE 6234 if (n.nodeName == 'OBJECT') 6235 return n.attributes; 6236 6237 // IE doesn't keep the selected attribute if you clone option elements 6238 if (n.nodeName === 'OPTION' && this.getAttrib(n, 'selected')) 6239 o.push({specified : 1, nodeName : 'selected'}); 6240 6241 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 6242 n.cloneNode(false).outerHTML.replace(/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi, '').replace(/[\w:\-]+/gi, function(a) { 6243 o.push({specified : 1, nodeName : a}); 6244 }); 6245 6246 return o; 6247 } 6248 6249 return n.attributes; 6250 }, 6251 6252 isEmpty : function(node, elements) { 6253 var self = this, i, attributes, type, walker, name, brCount = 0; 6254 6255 node = node.firstChild; 6256 if (node) { 6257 walker = new tinymce.dom.TreeWalker(node, node.parentNode); 6258 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; 6259 6260 do { 6261 type = node.nodeType; 6262 6263 if (type === 1) { 6264 // Ignore bogus elements 6265 if (node.getAttribute('data-mce-bogus')) 6266 continue; 6267 6268 // Keep empty elements like <img /> 6269 name = node.nodeName.toLowerCase(); 6270 if (elements && elements[name]) { 6271 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 6272 if (name === 'br') { 6273 brCount++; 6274 continue; 6275 } 6276 6277 return false; 6278 } 6279 6280 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 6281 attributes = self.getAttribs(node); 6282 i = node.attributes.length; 6283 while (i--) { 6284 name = node.attributes[i].nodeName; 6285 if (name === "name" || name === 'data-mce-bookmark') 6286 return false; 6287 } 6288 } 6289 6290 // Keep comment nodes 6291 if (type == 8) 6292 return false; 6293 6294 // Keep non whitespace text nodes 6295 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) 6296 return false; 6297 } while (node = walker.next()); 6298 } 6299 6300 return brCount <= 1; 6301 }, 6302 6303 destroy : function(s) { 6304 var t = this; 6305 6306 t.win = t.doc = t.root = t.events = t.frag = null; 6307 6308 // Manual destroy then remove unload handler 6309 if (!s) 6310 tinymce.removeUnload(t.destroy); 6311 }, 6312 6313 createRng : function() { 6314 var d = this.doc; 6315 6316 return d.createRange ? d.createRange() : new tinymce.dom.Range(this); 6317 }, 6318 6319 nodeIndex : function(node, normalized) { 6320 var idx = 0, lastNodeType, lastNode, nodeType; 6321 6322 if (node) { 6323 for (lastNodeType = node.nodeType, node = node.previousSibling, lastNode = node; node; node = node.previousSibling) { 6324 nodeType = node.nodeType; 6325 6326 // Normalize text nodes 6327 if (normalized && nodeType == 3) { 6328 if (nodeType == lastNodeType || !node.nodeValue.length) 6329 continue; 6330 } 6331 idx++; 6332 lastNodeType = nodeType; 6333 } 6334 } 6335 6336 return idx; 6337 }, 6338 6339 split : function(pe, e, re) { 6340 var t = this, r = t.createRng(), bef, aft, pa; 6341 6342 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents, this makes sense 6343 // but we don't want that in our code since it serves no purpose for the end user 6344 // For example if this is chopped: 6345 // <p>text 1<span><b>CHOP</b></span>text 2</p> 6346 // would produce: 6347 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 6348 // this function will then trim of empty edges and produce: 6349 // <p>text 1</p><b>CHOP</b><p>text 2</p> 6350 function trim(node) { 6351 var i, children = node.childNodes, type = node.nodeType; 6352 6353 function surroundedBySpans(node) { 6354 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 6355 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 6356 return previousIsSpan && nextIsSpan; 6357 } 6358 6359 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') 6360 return; 6361 6362 for (i = children.length - 1; i >= 0; i--) 6363 trim(children[i]); 6364 6365 if (type != 9) { 6366 // Keep non whitespace text nodes 6367 if (type == 3 && node.nodeValue.length > 0) { 6368 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 6369 // Also keep text nodes with only spaces if surrounded by spans. 6370 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 6371 var trimmedLength = tinymce.trim(node.nodeValue).length; 6372 if (!t.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) 6373 return; 6374 } else if (type == 1) { 6375 // If the only child is a bookmark then move it up 6376 children = node.childNodes; 6377 if (children.length == 1 && children[0] && children[0].nodeType == 1 && children[0].getAttribute('data-mce-type') == 'bookmark') 6378 node.parentNode.insertBefore(children[0], node); 6379 6380 // Keep non empty elements or img, hr etc 6381 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) 6382 return; 6383 } 6384 6385 t.remove(node); 6386 } 6387 6388 return node; 6389 }; 6390 6391 if (pe && e) { 6392 // Get before chunk 6393 r.setStart(pe.parentNode, t.nodeIndex(pe)); 6394 r.setEnd(e.parentNode, t.nodeIndex(e)); 6395 bef = r.extractContents(); 6396 6397 // Get after chunk 6398 r = t.createRng(); 6399 r.setStart(e.parentNode, t.nodeIndex(e) + 1); 6400 r.setEnd(pe.parentNode, t.nodeIndex(pe) + 1); 6401 aft = r.extractContents(); 6402 6403 // Insert before chunk 6404 pa = pe.parentNode; 6405 pa.insertBefore(trim(bef), pe); 6406 6407 // Insert middle chunk 6408 if (re) 6409 pa.replaceChild(re, e); 6410 else 6411 pa.insertBefore(e, pe); 6412 6413 // Insert after chunk 6414 pa.insertBefore(trim(aft), pe); 6415 t.remove(pe); 6416 6417 return re || e; 6418 } 6419 }, 6420 6421 bind : function(target, name, func, scope) { 6422 return this.events.add(target, name, func, scope || this); 6423 }, 6424 6425 unbind : function(target, name, func) { 6426 return this.events.remove(target, name, func); 6427 }, 6428 6429 fire : function(target, name, evt) { 6430 return this.events.fire(target, name, evt); 6431 }, 6432 6433 // Returns the content editable state of a node 6434 getContentEditable: function(node) { 6435 var contentEditable; 6436 6437 // Check type 6438 if (node.nodeType != 1) { 6439 return null; 6440 } 6441 6442 // Check for fake content editable 6443 contentEditable = node.getAttribute("data-mce-contenteditable"); 6444 if (contentEditable && contentEditable !== "inherit") { 6445 return contentEditable; 6446 } 6447 6448 // Check for real content editable 6449 return node.contentEditable !== "inherit" ? node.contentEditable : null; 6450 }, 6451 6452 6453 _findSib : function(node, selector, name) { 6454 var t = this, f = selector; 6455 6456 if (node) { 6457 // If expression make a function of it using is 6458 if (is(f, 'string')) { 6459 f = function(node) { 6460 return t.is(node, selector); 6461 }; 6462 } 6463 6464 // Loop all siblings 6465 for (node = node[name]; node; node = node[name]) { 6466 if (f(node)) 6467 return node; 6468 } 6469 } 6470 6471 return null; 6472 }, 6473 6474 _isRes : function(c) { 6475 // Is live resizble element 6476 return /^(top|left|bottom|right|width|height)/i.test(c) || /;\s*(top|left|bottom|right|width|height)/i.test(c); 6477 } 6478 6479 /* 6480 walk : function(n, f, s) { 6481 var d = this.doc, w; 6482 6483 if (d.createTreeWalker) { 6484 w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false); 6485 6486 while ((n = w.nextNode()) != null) 6487 f.call(s || this, n); 6488 } else 6489 tinymce.walk(n, f, 'childNodes', s); 6490 } 6491 */ 6492 6493 /* 6494 toRGB : function(s) { 6495 var c = /^\s*?#([0-9A-F]{2})([0-9A-F]{1,2})([0-9A-F]{2})?\s*?$/.exec(s); 6496 6497 if (c) { 6498 // #FFF -> #FFFFFF 6499 if (!is(c[3])) 6500 c[3] = c[2] = c[1]; 6501 6502 return "rgb(" + parseInt(c[1], 16) + "," + parseInt(c[2], 16) + "," + parseInt(c[3], 16) + ")"; 6503 } 6504 6505 return s; 6506 } 6507 */ 6508 }); 6509 6510 tinymce.DOM = new tinymce.dom.DOMUtils(document, {process_html : 0}); 6511 })(tinymce); 6512 6513 (function(ns) { 6514 // Range constructor 6515 function Range(dom) { 6516 var t = this, 6517 doc = dom.doc, 6518 EXTRACT = 0, 6519 CLONE = 1, 6520 DELETE = 2, 6521 TRUE = true, 6522 FALSE = false, 6523 START_OFFSET = 'startOffset', 6524 START_CONTAINER = 'startContainer', 6525 END_CONTAINER = 'endContainer', 6526 END_OFFSET = 'endOffset', 6527 extend = tinymce.extend, 6528 nodeIndex = dom.nodeIndex; 6529 6530 extend(t, { 6531 // Inital states 6532 startContainer : doc, 6533 startOffset : 0, 6534 endContainer : doc, 6535 endOffset : 0, 6536 collapsed : TRUE, 6537 commonAncestorContainer : doc, 6538 6539 // Range constants 6540 START_TO_START : 0, 6541 START_TO_END : 1, 6542 END_TO_END : 2, 6543 END_TO_START : 3, 6544 6545 // Public methods 6546 setStart : setStart, 6547 setEnd : setEnd, 6548 setStartBefore : setStartBefore, 6549 setStartAfter : setStartAfter, 6550 setEndBefore : setEndBefore, 6551 setEndAfter : setEndAfter, 6552 collapse : collapse, 6553 selectNode : selectNode, 6554 selectNodeContents : selectNodeContents, 6555 compareBoundaryPoints : compareBoundaryPoints, 6556 deleteContents : deleteContents, 6557 extractContents : extractContents, 6558 cloneContents : cloneContents, 6559 insertNode : insertNode, 6560 surroundContents : surroundContents, 6561 cloneRange : cloneRange, 6562 toStringIE : toStringIE 6563 }); 6564 6565 function createDocumentFragment() { 6566 return doc.createDocumentFragment(); 6567 }; 6568 6569 function setStart(n, o) { 6570 _setEndPoint(TRUE, n, o); 6571 }; 6572 6573 function setEnd(n, o) { 6574 _setEndPoint(FALSE, n, o); 6575 }; 6576 6577 function setStartBefore(n) { 6578 setStart(n.parentNode, nodeIndex(n)); 6579 }; 6580 6581 function setStartAfter(n) { 6582 setStart(n.parentNode, nodeIndex(n) + 1); 6583 }; 6584 6585 function setEndBefore(n) { 6586 setEnd(n.parentNode, nodeIndex(n)); 6587 }; 6588 6589 function setEndAfter(n) { 6590 setEnd(n.parentNode, nodeIndex(n) + 1); 6591 }; 6592 6593 function collapse(ts) { 6594 if (ts) { 6595 t[END_CONTAINER] = t[START_CONTAINER]; 6596 t[END_OFFSET] = t[START_OFFSET]; 6597 } else { 6598 t[START_CONTAINER] = t[END_CONTAINER]; 6599 t[START_OFFSET] = t[END_OFFSET]; 6600 } 6601 6602 t.collapsed = TRUE; 6603 }; 6604 6605 function selectNode(n) { 6606 setStartBefore(n); 6607 setEndAfter(n); 6608 }; 6609 6610 function selectNodeContents(n) { 6611 setStart(n, 0); 6612 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 6613 }; 6614 6615 function compareBoundaryPoints(h, r) { 6616 var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET], 6617 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 6618 6619 // Check START_TO_START 6620 if (h === 0) 6621 return _compareBoundaryPoints(sc, so, rsc, rso); 6622 6623 // Check START_TO_END 6624 if (h === 1) 6625 return _compareBoundaryPoints(ec, eo, rsc, rso); 6626 6627 // Check END_TO_END 6628 if (h === 2) 6629 return _compareBoundaryPoints(ec, eo, rec, reo); 6630 6631 // Check END_TO_START 6632 if (h === 3) 6633 return _compareBoundaryPoints(sc, so, rec, reo); 6634 }; 6635 6636 function deleteContents() { 6637 _traverse(DELETE); 6638 }; 6639 6640 function extractContents() { 6641 return _traverse(EXTRACT); 6642 }; 6643 6644 function cloneContents() { 6645 return _traverse(CLONE); 6646 }; 6647 6648 function insertNode(n) { 6649 var startContainer = this[START_CONTAINER], 6650 startOffset = this[START_OFFSET], nn, o; 6651 6652 // Node is TEXT_NODE or CDATA 6653 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 6654 if (!startOffset) { 6655 // At the start of text 6656 startContainer.parentNode.insertBefore(n, startContainer); 6657 } else if (startOffset >= startContainer.nodeValue.length) { 6658 // At the end of text 6659 dom.insertAfter(n, startContainer); 6660 } else { 6661 // Middle, need to split 6662 nn = startContainer.splitText(startOffset); 6663 startContainer.parentNode.insertBefore(n, nn); 6664 } 6665 } else { 6666 // Insert element node 6667 if (startContainer.childNodes.length > 0) 6668 o = startContainer.childNodes[startOffset]; 6669 6670 if (o) 6671 startContainer.insertBefore(n, o); 6672 else 6673 startContainer.appendChild(n); 6674 } 6675 }; 6676 6677 function surroundContents(n) { 6678 var f = t.extractContents(); 6679 6680 t.insertNode(n); 6681 n.appendChild(f); 6682 t.selectNode(n); 6683 }; 6684 6685 function cloneRange() { 6686 return extend(new Range(dom), { 6687 startContainer : t[START_CONTAINER], 6688 startOffset : t[START_OFFSET], 6689 endContainer : t[END_CONTAINER], 6690 endOffset : t[END_OFFSET], 6691 collapsed : t.collapsed, 6692 commonAncestorContainer : t.commonAncestorContainer 6693 }); 6694 }; 6695 6696 // Private methods 6697 6698 function _getSelectedNode(container, offset) { 6699 var child; 6700 6701 if (container.nodeType == 3 /* TEXT_NODE */) 6702 return container; 6703 6704 if (offset < 0) 6705 return container; 6706 6707 child = container.firstChild; 6708 while (child && offset > 0) { 6709 --offset; 6710 child = child.nextSibling; 6711 } 6712 6713 if (child) 6714 return child; 6715 6716 return container; 6717 }; 6718 6719 function _isCollapsed() { 6720 return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]); 6721 }; 6722 6723 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 6724 var c, offsetC, n, cmnRoot, childA, childB; 6725 6726 // In the first case the boundary-points have the same container. A is before B 6727 // if its offset is less than the offset of B, A is equal to B if its offset is 6728 // equal to the offset of B, and A is after B if its offset is greater than the 6729 // offset of B. 6730 if (containerA == containerB) { 6731 if (offsetA == offsetB) 6732 return 0; // equal 6733 6734 if (offsetA < offsetB) 6735 return -1; // before 6736 6737 return 1; // after 6738 } 6739 6740 // In the second case a child node C of the container of A is an ancestor 6741 // container of B. In this case, A is before B if the offset of A is less than or 6742 // equal to the index of the child node C and A is after B otherwise. 6743 c = containerB; 6744 while (c && c.parentNode != containerA) 6745 c = c.parentNode; 6746 6747 if (c) { 6748 offsetC = 0; 6749 n = containerA.firstChild; 6750 6751 while (n != c && offsetC < offsetA) { 6752 offsetC++; 6753 n = n.nextSibling; 6754 } 6755 6756 if (offsetA <= offsetC) 6757 return -1; // before 6758 6759 return 1; // after 6760 } 6761 6762 // In the third case a child node C of the container of B is an ancestor container 6763 // of A. In this case, A is before B if the index of the child node C is less than 6764 // the offset of B and A is after B otherwise. 6765 c = containerA; 6766 while (c && c.parentNode != containerB) { 6767 c = c.parentNode; 6768 } 6769 6770 if (c) { 6771 offsetC = 0; 6772 n = containerB.firstChild; 6773 6774 while (n != c && offsetC < offsetB) { 6775 offsetC++; 6776 n = n.nextSibling; 6777 } 6778 6779 if (offsetC < offsetB) 6780 return -1; // before 6781 6782 return 1; // after 6783 } 6784 6785 // In the fourth case, none of three other cases hold: the containers of A and B 6786 // are siblings or descendants of sibling nodes. In this case, A is before B if 6787 // the container of A is before the container of B in a pre-order traversal of the 6788 // Ranges' context tree and A is after B otherwise. 6789 cmnRoot = dom.findCommonAncestor(containerA, containerB); 6790 childA = containerA; 6791 6792 while (childA && childA.parentNode != cmnRoot) 6793 childA = childA.parentNode; 6794 6795 if (!childA) 6796 childA = cmnRoot; 6797 6798 childB = containerB; 6799 while (childB && childB.parentNode != cmnRoot) 6800 childB = childB.parentNode; 6801 6802 if (!childB) 6803 childB = cmnRoot; 6804 6805 if (childA == childB) 6806 return 0; // equal 6807 6808 n = cmnRoot.firstChild; 6809 while (n) { 6810 if (n == childA) 6811 return -1; // before 6812 6813 if (n == childB) 6814 return 1; // after 6815 6816 n = n.nextSibling; 6817 } 6818 }; 6819 6820 function _setEndPoint(st, n, o) { 6821 var ec, sc; 6822 6823 if (st) { 6824 t[START_CONTAINER] = n; 6825 t[START_OFFSET] = o; 6826 } else { 6827 t[END_CONTAINER] = n; 6828 t[END_OFFSET] = o; 6829 } 6830 6831 // If one boundary-point of a Range is set to have a root container 6832 // other than the current one for the Range, the Range is collapsed to 6833 // the new position. This enforces the restriction that both boundary- 6834 // points of a Range must have the same root container. 6835 ec = t[END_CONTAINER]; 6836 while (ec.parentNode) 6837 ec = ec.parentNode; 6838 6839 sc = t[START_CONTAINER]; 6840 while (sc.parentNode) 6841 sc = sc.parentNode; 6842 6843 if (sc == ec) { 6844 // The start position of a Range is guaranteed to never be after the 6845 // end position. To enforce this restriction, if the start is set to 6846 // be at a position after the end, the Range is collapsed to that 6847 // position. 6848 if (_compareBoundaryPoints(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) 6849 t.collapse(st); 6850 } else 6851 t.collapse(st); 6852 6853 t.collapsed = _isCollapsed(); 6854 t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]); 6855 }; 6856 6857 function _traverse(how) { 6858 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 6859 6860 if (t[START_CONTAINER] == t[END_CONTAINER]) 6861 return _traverseSameContainer(how); 6862 6863 for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6864 if (p == t[START_CONTAINER]) 6865 return _traverseCommonStartContainer(c, how); 6866 6867 ++endContainerDepth; 6868 } 6869 6870 for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 6871 if (p == t[END_CONTAINER]) 6872 return _traverseCommonEndContainer(c, how); 6873 6874 ++startContainerDepth; 6875 } 6876 6877 depthDiff = startContainerDepth - endContainerDepth; 6878 6879 startNode = t[START_CONTAINER]; 6880 while (depthDiff > 0) { 6881 startNode = startNode.parentNode; 6882 depthDiff--; 6883 } 6884 6885 endNode = t[END_CONTAINER]; 6886 while (depthDiff < 0) { 6887 endNode = endNode.parentNode; 6888 depthDiff++; 6889 } 6890 6891 // ascend the ancestor hierarchy until we have a common parent. 6892 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 6893 startNode = sp; 6894 endNode = ep; 6895 } 6896 6897 return _traverseCommonAncestors(startNode, endNode, how); 6898 }; 6899 6900 function _traverseSameContainer(how) { 6901 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 6902 6903 if (how != DELETE) 6904 frag = createDocumentFragment(); 6905 6906 // If selection is empty, just return the fragment 6907 if (t[START_OFFSET] == t[END_OFFSET]) 6908 return frag; 6909 6910 // Text node needs special case handling 6911 if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 6912 // get the substring 6913 s = t[START_CONTAINER].nodeValue; 6914 sub = s.substring(t[START_OFFSET], t[END_OFFSET]); 6915 6916 // set the original text node to its new value 6917 if (how != CLONE) { 6918 n = t[START_CONTAINER]; 6919 start = t[START_OFFSET]; 6920 len = t[END_OFFSET] - t[START_OFFSET]; 6921 6922 if (start === 0 && len >= n.nodeValue.length - 1) { 6923 n.parentNode.removeChild(n); 6924 } else { 6925 n.deleteData(start, len); 6926 } 6927 6928 // Nothing is partially selected, so collapse to start point 6929 t.collapse(TRUE); 6930 } 6931 6932 if (how == DELETE) 6933 return; 6934 6935 if (sub.length > 0) { 6936 frag.appendChild(doc.createTextNode(sub)); 6937 } 6938 6939 return frag; 6940 } 6941 6942 // Copy nodes between the start/end offsets. 6943 n = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]); 6944 cnt = t[END_OFFSET] - t[START_OFFSET]; 6945 6946 while (n && cnt > 0) { 6947 sibling = n.nextSibling; 6948 xferNode = _traverseFullySelected(n, how); 6949 6950 if (frag) 6951 frag.appendChild( xferNode ); 6952 6953 --cnt; 6954 n = sibling; 6955 } 6956 6957 // Nothing is partially selected, so collapse to start point 6958 if (how != CLONE) 6959 t.collapse(TRUE); 6960 6961 return frag; 6962 }; 6963 6964 function _traverseCommonStartContainer(endAncestor, how) { 6965 var frag, n, endIdx, cnt, sibling, xferNode; 6966 6967 if (how != DELETE) 6968 frag = createDocumentFragment(); 6969 6970 n = _traverseRightBoundary(endAncestor, how); 6971 6972 if (frag) 6973 frag.appendChild(n); 6974 6975 endIdx = nodeIndex(endAncestor); 6976 cnt = endIdx - t[START_OFFSET]; 6977 6978 if (cnt <= 0) { 6979 // Collapse to just before the endAncestor, which 6980 // is partially selected. 6981 if (how != CLONE) { 6982 t.setEndBefore(endAncestor); 6983 t.collapse(FALSE); 6984 } 6985 6986 return frag; 6987 } 6988 6989 n = endAncestor.previousSibling; 6990 while (cnt > 0) { 6991 sibling = n.previousSibling; 6992 xferNode = _traverseFullySelected(n, how); 6993 6994 if (frag) 6995 frag.insertBefore(xferNode, frag.firstChild); 6996 6997 --cnt; 6998 n = sibling; 6999 } 7000 7001 // Collapse to just before the endAncestor, which 7002 // is partially selected. 7003 if (how != CLONE) { 7004 t.setEndBefore(endAncestor); 7005 t.collapse(FALSE); 7006 } 7007 7008 return frag; 7009 }; 7010 7011 function _traverseCommonEndContainer(startAncestor, how) { 7012 var frag, startIdx, n, cnt, sibling, xferNode; 7013 7014 if (how != DELETE) 7015 frag = createDocumentFragment(); 7016 7017 n = _traverseLeftBoundary(startAncestor, how); 7018 if (frag) 7019 frag.appendChild(n); 7020 7021 startIdx = nodeIndex(startAncestor); 7022 ++startIdx; // Because we already traversed it 7023 7024 cnt = t[END_OFFSET] - startIdx; 7025 n = startAncestor.nextSibling; 7026 while (n && cnt > 0) { 7027 sibling = n.nextSibling; 7028 xferNode = _traverseFullySelected(n, how); 7029 7030 if (frag) 7031 frag.appendChild(xferNode); 7032 7033 --cnt; 7034 n = sibling; 7035 } 7036 7037 if (how != CLONE) { 7038 t.setStartAfter(startAncestor); 7039 t.collapse(TRUE); 7040 } 7041 7042 return frag; 7043 }; 7044 7045 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 7046 var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling; 7047 7048 if (how != DELETE) 7049 frag = createDocumentFragment(); 7050 7051 n = _traverseLeftBoundary(startAncestor, how); 7052 if (frag) 7053 frag.appendChild(n); 7054 7055 commonParent = startAncestor.parentNode; 7056 startOffset = nodeIndex(startAncestor); 7057 endOffset = nodeIndex(endAncestor); 7058 ++startOffset; 7059 7060 cnt = endOffset - startOffset; 7061 sibling = startAncestor.nextSibling; 7062 7063 while (cnt > 0) { 7064 nextSibling = sibling.nextSibling; 7065 n = _traverseFullySelected(sibling, how); 7066 7067 if (frag) 7068 frag.appendChild(n); 7069 7070 sibling = nextSibling; 7071 --cnt; 7072 } 7073 7074 n = _traverseRightBoundary(endAncestor, how); 7075 7076 if (frag) 7077 frag.appendChild(n); 7078 7079 if (how != CLONE) { 7080 t.setStartAfter(startAncestor); 7081 t.collapse(TRUE); 7082 } 7083 7084 return frag; 7085 }; 7086 7087 function _traverseRightBoundary(root, how) { 7088 var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent, prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[END_CONTAINER]; 7089 7090 if (next == root) 7091 return _traverseNode(next, isFullySelected, FALSE, how); 7092 7093 parent = next.parentNode; 7094 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 7095 7096 while (parent) { 7097 while (next) { 7098 prevSibling = next.previousSibling; 7099 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 7100 7101 if (how != DELETE) 7102 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 7103 7104 isFullySelected = TRUE; 7105 next = prevSibling; 7106 } 7107 7108 if (parent == root) 7109 return clonedParent; 7110 7111 next = parent.previousSibling; 7112 parent = parent.parentNode; 7113 7114 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 7115 7116 if (how != DELETE) 7117 clonedGrandParent.appendChild(clonedParent); 7118 7119 clonedParent = clonedGrandParent; 7120 } 7121 }; 7122 7123 function _traverseLeftBoundary(root, how) { 7124 var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[START_CONTAINER], parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 7125 7126 if (next == root) 7127 return _traverseNode(next, isFullySelected, TRUE, how); 7128 7129 parent = next.parentNode; 7130 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 7131 7132 while (parent) { 7133 while (next) { 7134 nextSibling = next.nextSibling; 7135 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 7136 7137 if (how != DELETE) 7138 clonedParent.appendChild(clonedChild); 7139 7140 isFullySelected = TRUE; 7141 next = nextSibling; 7142 } 7143 7144 if (parent == root) 7145 return clonedParent; 7146 7147 next = parent.nextSibling; 7148 parent = parent.parentNode; 7149 7150 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 7151 7152 if (how != DELETE) 7153 clonedGrandParent.appendChild(clonedParent); 7154 7155 clonedParent = clonedGrandParent; 7156 } 7157 }; 7158 7159 function _traverseNode(n, isFullySelected, isLeft, how) { 7160 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 7161 7162 if (isFullySelected) 7163 return _traverseFullySelected(n, how); 7164 7165 if (n.nodeType == 3 /* TEXT_NODE */) { 7166 txtValue = n.nodeValue; 7167 7168 if (isLeft) { 7169 offset = t[START_OFFSET]; 7170 newNodeValue = txtValue.substring(offset); 7171 oldNodeValue = txtValue.substring(0, offset); 7172 } else { 7173 offset = t[END_OFFSET]; 7174 newNodeValue = txtValue.substring(0, offset); 7175 oldNodeValue = txtValue.substring(offset); 7176 } 7177 7178 if (how != CLONE) 7179 n.nodeValue = oldNodeValue; 7180 7181 if (how == DELETE) 7182 return; 7183 7184 newNode = dom.clone(n, FALSE); 7185 newNode.nodeValue = newNodeValue; 7186 7187 return newNode; 7188 } 7189 7190 if (how == DELETE) 7191 return; 7192 7193 return dom.clone(n, FALSE); 7194 }; 7195 7196 function _traverseFullySelected(n, how) { 7197 if (how != DELETE) 7198 return how == CLONE ? dom.clone(n, TRUE) : n; 7199 7200 n.parentNode.removeChild(n); 7201 }; 7202 7203 function toStringIE() { 7204 return dom.create('body', null, cloneContents()).outerText; 7205 } 7206 7207 return t; 7208 }; 7209 7210 ns.Range = Range; 7211 7212 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 7213 Range.prototype.toString = function() { 7214 return this.toStringIE(); 7215 }; 7216 })(tinymce.dom); 7217 7218 (function() { 7219 function Selection(selection) { 7220 var self = this, dom = selection.dom, TRUE = true, FALSE = false; 7221 7222 function getPosition(rng, start) { 7223 var checkRng, startIndex = 0, endIndex, inside, 7224 children, child, offset, index, position = -1, parent; 7225 7226 // Setup test range, collapse it and get the parent 7227 checkRng = rng.duplicate(); 7228 checkRng.collapse(start); 7229 parent = checkRng.parentElement(); 7230 7231 // Check if the selection is within the right document 7232 if (parent.ownerDocument !== selection.dom.doc) 7233 return; 7234 7235 // IE will report non editable elements as it's parent so look for an editable one 7236 while (parent.contentEditable === "false") { 7237 parent = parent.parentNode; 7238 } 7239 7240 // If parent doesn't have any children then return that we are inside the element 7241 if (!parent.hasChildNodes()) { 7242 return {node : parent, inside : 1}; 7243 } 7244 7245 // Setup node list and endIndex 7246 children = parent.children; 7247 endIndex = children.length - 1; 7248 7249 // Perform a binary search for the position 7250 while (startIndex <= endIndex) { 7251 index = Math.floor((startIndex + endIndex) / 2); 7252 7253 // Move selection to node and compare the ranges 7254 child = children[index]; 7255 checkRng.moveToElementText(child); 7256 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 7257 7258 // Before/after or an exact match 7259 if (position > 0) { 7260 endIndex = index - 1; 7261 } else if (position < 0) { 7262 startIndex = index + 1; 7263 } else { 7264 return {node : child}; 7265 } 7266 } 7267 7268 // Check if child position is before or we didn't find a position 7269 if (position < 0) { 7270 // No element child was found use the parent element and the offset inside that 7271 if (!child) { 7272 checkRng.moveToElementText(parent); 7273 checkRng.collapse(true); 7274 child = parent; 7275 inside = true; 7276 } else 7277 checkRng.collapse(false); 7278 7279 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7280 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 7281 offset = 0; 7282 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7283 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 7284 break; 7285 } 7286 7287 offset++; 7288 } 7289 } else { 7290 // Child position is after the selection endpoint 7291 checkRng.collapse(true); 7292 7293 // Walk character by character in text node until we hit the selected range endpoint, hit the end of document or parent isn't the right one 7294 offset = 0; 7295 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 7296 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 7297 break; 7298 } 7299 7300 offset++; 7301 } 7302 } 7303 7304 return {node : child, position : position, offset : offset, inside : inside}; 7305 }; 7306 7307 // Returns a W3C DOM compatible range object by using the IE Range API 7308 function getRange() { 7309 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark, fail; 7310 7311 // If selection is outside the current document just return an empty range 7312 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 7313 if (element.ownerDocument != dom.doc) 7314 return domRange; 7315 7316 collapsed = selection.isCollapsed(); 7317 7318 // Handle control selection 7319 if (ieRange.item) { 7320 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 7321 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 7322 7323 return domRange; 7324 } 7325 7326 function findEndPoint(start) { 7327 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 7328 7329 container = endPoint.node; 7330 offset = endPoint.offset; 7331 7332 if (endPoint.inside && !container.hasChildNodes()) { 7333 domRange[start ? 'setStart' : 'setEnd'](container, 0); 7334 return; 7335 } 7336 7337 if (offset === undef) { 7338 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 7339 return; 7340 } 7341 7342 if (endPoint.position < 0) { 7343 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 7344 7345 if (!sibling) { 7346 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 7347 return; 7348 } 7349 7350 if (!offset) { 7351 if (sibling.nodeType == 3) 7352 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 7353 else 7354 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 7355 7356 return; 7357 } 7358 7359 // Find the text node and offset 7360 while (sibling) { 7361 nodeValue = sibling.nodeValue; 7362 textNodeOffset += nodeValue.length; 7363 7364 // We are at or passed the position we where looking for 7365 if (textNodeOffset >= offset) { 7366 container = sibling; 7367 textNodeOffset -= offset; 7368 textNodeOffset = nodeValue.length - textNodeOffset; 7369 break; 7370 } 7371 7372 sibling = sibling.nextSibling; 7373 } 7374 } else { 7375 // Find the text node and offset 7376 sibling = container.previousSibling; 7377 7378 if (!sibling) 7379 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 7380 7381 // If there isn't any text to loop then use the first position 7382 if (!offset) { 7383 if (container.nodeType == 3) 7384 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 7385 else 7386 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 7387 7388 return; 7389 } 7390 7391 while (sibling) { 7392 textNodeOffset += sibling.nodeValue.length; 7393 7394 // We are at or passed the position we where looking for 7395 if (textNodeOffset >= offset) { 7396 container = sibling; 7397 textNodeOffset -= offset; 7398 break; 7399 } 7400 7401 sibling = sibling.previousSibling; 7402 } 7403 } 7404 7405 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 7406 }; 7407 7408 try { 7409 // Find start point 7410 findEndPoint(true); 7411 7412 // Find end point if needed 7413 if (!collapsed) 7414 findEndPoint(); 7415 } catch (ex) { 7416 // IE has a nasty bug where text nodes might throw "invalid argument" when you 7417 // access the nodeValue or other properties of text nodes. This seems to happend when 7418 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 7419 if (ex.number == -2147024809) { 7420 // Get the current selection 7421 bookmark = self.getBookmark(2); 7422 7423 // Get start element 7424 tmpRange = ieRange.duplicate(); 7425 tmpRange.collapse(true); 7426 element = tmpRange.parentElement(); 7427 7428 // Get end element 7429 if (!collapsed) { 7430 tmpRange = ieRange.duplicate(); 7431 tmpRange.collapse(false); 7432 element2 = tmpRange.parentElement(); 7433 element2.innerHTML = element2.innerHTML; 7434 } 7435 7436 // Remove the broken elements 7437 element.innerHTML = element.innerHTML; 7438 7439 // Restore the selection 7440 self.moveToBookmark(bookmark); 7441 7442 // Since the range has moved we need to re-get it 7443 ieRange = selection.getRng(); 7444 7445 // Find start point 7446 findEndPoint(true); 7447 7448 // Find end point if needed 7449 if (!collapsed) 7450 findEndPoint(); 7451 } else 7452 throw ex; // Throw other errors 7453 } 7454 7455 return domRange; 7456 }; 7457 7458 this.getBookmark = function(type) { 7459 var rng = selection.getRng(), start, end, bookmark = {}; 7460 7461 function getIndexes(node) { 7462 var parent, root, children, i, indexes = []; 7463 7464 parent = node.parentNode; 7465 root = dom.getRoot().parentNode; 7466 7467 while (parent != root && parent.nodeType !== 9) { 7468 children = parent.children; 7469 7470 i = children.length; 7471 while (i--) { 7472 if (node === children[i]) { 7473 indexes.push(i); 7474 break; 7475 } 7476 } 7477 7478 node = parent; 7479 parent = parent.parentNode; 7480 } 7481 7482 return indexes; 7483 }; 7484 7485 function getBookmarkEndPoint(start) { 7486 var position; 7487 7488 position = getPosition(rng, start); 7489 if (position) { 7490 return { 7491 position : position.position, 7492 offset : position.offset, 7493 indexes : getIndexes(position.node), 7494 inside : position.inside 7495 }; 7496 } 7497 }; 7498 7499 // Non ubstructive bookmark 7500 if (type === 2) { 7501 // Handle text selection 7502 if (!rng.item) { 7503 bookmark.start = getBookmarkEndPoint(true); 7504 7505 if (!selection.isCollapsed()) 7506 bookmark.end = getBookmarkEndPoint(); 7507 } else 7508 bookmark.start = {ctrl : true, indexes : getIndexes(rng.item(0))}; 7509 } 7510 7511 return bookmark; 7512 }; 7513 7514 this.moveToBookmark = function(bookmark) { 7515 var rng, body = dom.doc.body; 7516 7517 function resolveIndexes(indexes) { 7518 var node, i, idx, children; 7519 7520 node = dom.getRoot(); 7521 for (i = indexes.length - 1; i >= 0; i--) { 7522 children = node.children; 7523 idx = indexes[i]; 7524 7525 if (idx <= children.length - 1) { 7526 node = children[idx]; 7527 } 7528 } 7529 7530 return node; 7531 }; 7532 7533 function setBookmarkEndPoint(start) { 7534 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef; 7535 7536 if (endPoint) { 7537 moveLeft = endPoint.position > 0; 7538 7539 moveRng = body.createTextRange(); 7540 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 7541 7542 offset = endPoint.offset; 7543 if (offset !== undef) { 7544 moveRng.collapse(endPoint.inside || moveLeft); 7545 moveRng.moveStart('character', moveLeft ? -offset : offset); 7546 } else 7547 moveRng.collapse(start); 7548 7549 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 7550 7551 if (start) 7552 rng.collapse(true); 7553 } 7554 }; 7555 7556 if (bookmark.start) { 7557 if (bookmark.start.ctrl) { 7558 rng = body.createControlRange(); 7559 rng.addElement(resolveIndexes(bookmark.start.indexes)); 7560 rng.select(); 7561 } else { 7562 rng = body.createTextRange(); 7563 setBookmarkEndPoint(true); 7564 setBookmarkEndPoint(); 7565 rng.select(); 7566 } 7567 } 7568 }; 7569 7570 this.addRange = function(rng) { 7571 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, doc = selection.dom.doc, body = doc.body; 7572 7573 function setEndPoint(start) { 7574 var container, offset, marker, tmpRng, nodes; 7575 7576 marker = dom.create('a'); 7577 container = start ? startContainer : endContainer; 7578 offset = start ? startOffset : endOffset; 7579 tmpRng = ieRng.duplicate(); 7580 7581 if (container == doc || container == doc.documentElement) { 7582 container = body; 7583 offset = 0; 7584 } 7585 7586 if (container.nodeType == 3) { 7587 container.parentNode.insertBefore(marker, container); 7588 tmpRng.moveToElementText(marker); 7589 tmpRng.moveStart('character', offset); 7590 dom.remove(marker); 7591 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7592 } else { 7593 nodes = container.childNodes; 7594 7595 if (nodes.length) { 7596 if (offset >= nodes.length) { 7597 dom.insertAfter(marker, nodes[nodes.length - 1]); 7598 } else { 7599 container.insertBefore(marker, nodes[offset]); 7600 } 7601 7602 tmpRng.moveToElementText(marker); 7603 } else if (container.canHaveHTML) { 7604 // Empty node selection for example <div>|</div> 7605 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 7606 container.innerHTML = '<span>\uFEFF</span>'; 7607 marker = container.firstChild; 7608 tmpRng.moveToElementText(marker); 7609 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 7610 } 7611 7612 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 7613 dom.remove(marker); 7614 } 7615 } 7616 7617 // Setup some shorter versions 7618 startContainer = rng.startContainer; 7619 startOffset = rng.startOffset; 7620 endContainer = rng.endContainer; 7621 endOffset = rng.endOffset; 7622 ieRng = body.createTextRange(); 7623 7624 // If single element selection then try making a control selection out of it 7625 if (startContainer == endContainer && startContainer.nodeType == 1) { 7626 // Trick to place the caret inside an empty block element like <p></p> 7627 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 7628 if (startContainer.canHaveHTML) { 7629 // Check if previous sibling is an empty block if it is then we need to render it 7630 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 7631 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 7632 sibling = startContainer.previousSibling; 7633 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 7634 sibling.innerHTML = '\uFEFF'; 7635 } else { 7636 sibling = null; 7637 } 7638 7639 startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>'; 7640 ieRng.moveToElementText(startContainer.lastChild); 7641 ieRng.select(); 7642 dom.doc.selection.clear(); 7643 startContainer.innerHTML = ''; 7644 7645 if (sibling) { 7646 sibling.innerHTML = ''; 7647 } 7648 return; 7649 } else { 7650 startOffset = dom.nodeIndex(startContainer); 7651 startContainer = startContainer.parentNode; 7652 } 7653 } 7654 7655 if (startOffset == endOffset - 1) { 7656 try { 7657 ctrlRng = body.createControlRange(); 7658 ctrlRng.addElement(startContainer.childNodes[startOffset]); 7659 ctrlRng.select(); 7660 return; 7661 } catch (ex) { 7662 // Ignore 7663 } 7664 } 7665 } 7666 7667 // Set start/end point of selection 7668 setEndPoint(true); 7669 setEndPoint(); 7670 7671 // Select the new range and scroll it into view 7672 ieRng.select(); 7673 }; 7674 7675 // Expose range method 7676 this.getRangeAt = getRange; 7677 }; 7678 7679 // Expose the selection object 7680 tinymce.dom.TridentSelection = Selection; 7681 })(); 7682 7683 7684 (function(tinymce) { 7685 tinymce.dom.Element = function(id, settings) { 7686 var t = this, dom, el; 7687 7688 t.settings = settings = settings || {}; 7689 t.id = id; 7690 t.dom = dom = settings.dom || tinymce.DOM; 7691 7692 // Only IE leaks DOM references, this is a lot faster 7693 if (!tinymce.isIE) 7694 el = dom.get(t.id); 7695 7696 tinymce.each( 7697 ('getPos,getRect,getParent,add,setStyle,getStyle,setStyles,' + 7698 'setAttrib,setAttribs,getAttrib,addClass,removeClass,' + 7699 'hasClass,getOuterHTML,setOuterHTML,remove,show,hide,' + 7700 'isHidden,setHTML,get').split(/,/), function(k) { 7701 t[k] = function() { 7702 var a = [id], i; 7703 7704 for (i = 0; i < arguments.length; i++) 7705 a.push(arguments[i]); 7706 7707 a = dom[k].apply(dom, a); 7708 t.update(k); 7709 7710 return a; 7711 }; 7712 } 7713 ); 7714 7715 tinymce.extend(t, { 7716 on : function(n, f, s) { 7717 return tinymce.dom.Event.add(t.id, n, f, s); 7718 }, 7719 7720 getXY : function() { 7721 return { 7722 x : parseInt(t.getStyle('left')), 7723 y : parseInt(t.getStyle('top')) 7724 }; 7725 }, 7726 7727 getSize : function() { 7728 var n = dom.get(t.id); 7729 7730 return { 7731 w : parseInt(t.getStyle('width') || n.clientWidth), 7732 h : parseInt(t.getStyle('height') || n.clientHeight) 7733 }; 7734 }, 7735 7736 moveTo : function(x, y) { 7737 t.setStyles({left : x, top : y}); 7738 }, 7739 7740 moveBy : function(x, y) { 7741 var p = t.getXY(); 7742 7743 t.moveTo(p.x + x, p.y + y); 7744 }, 7745 7746 resizeTo : function(w, h) { 7747 t.setStyles({width : w, height : h}); 7748 }, 7749 7750 resizeBy : function(w, h) { 7751 var s = t.getSize(); 7752 7753 t.resizeTo(s.w + w, s.h + h); 7754 }, 7755 7756 update : function(k) { 7757 var b; 7758 7759 if (tinymce.isIE6 && settings.blocker) { 7760 k = k || ''; 7761 7762 // Ignore getters 7763 if (k.indexOf('get') === 0 || k.indexOf('has') === 0 || k.indexOf('is') === 0) 7764 return; 7765 7766 // Remove blocker on remove 7767 if (k == 'remove') { 7768 dom.remove(t.blocker); 7769 return; 7770 } 7771 7772 if (!t.blocker) { 7773 t.blocker = dom.uniqueId(); 7774 b = dom.add(settings.container || dom.getRoot(), 'iframe', {id : t.blocker, style : 'position:absolute;', frameBorder : 0, src : 'javascript:""'}); 7775 dom.setStyle(b, 'opacity', 0); 7776 } else 7777 b = dom.get(t.blocker); 7778 7779 dom.setStyles(b, { 7780 left : t.getStyle('left', 1), 7781 top : t.getStyle('top', 1), 7782 width : t.getStyle('width', 1), 7783 height : t.getStyle('height', 1), 7784 display : t.getStyle('display', 1), 7785 zIndex : parseInt(t.getStyle('zIndex', 1) || 0) - 1 7786 }); 7787 } 7788 } 7789 }); 7790 }; 7791 })(tinymce); 7792 7793 (function(tinymce) { 7794 function trimNl(s) { 7795 return s.replace(/[\n\r]+/g, ''); 7796 }; 7797 7798 // Shorten names 7799 var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker; 7800 7801 tinymce.create('tinymce.dom.Selection', { 7802 Selection : function(dom, win, serializer, editor) { 7803 var t = this; 7804 7805 t.dom = dom; 7806 t.win = win; 7807 t.serializer = serializer; 7808 t.editor = editor; 7809 7810 // Add events 7811 each([ 7812 'onBeforeSetContent', 7813 7814 'onBeforeGetContent', 7815 7816 'onSetContent', 7817 7818 'onGetContent' 7819 ], function(e) { 7820 t[e] = new tinymce.util.Dispatcher(t); 7821 }); 7822 7823 // No W3C Range support 7824 if (!t.win.getSelection) 7825 t.tridentSel = new tinymce.dom.TridentSelection(t); 7826 7827 if (tinymce.isIE && dom.boxModel) 7828 this._fixIESelection(); 7829 7830 // Prevent leaks 7831 tinymce.addUnload(t.destroy, t); 7832 }, 7833 7834 setCursorLocation: function(node, offset) { 7835 var t = this; var r = t.dom.createRng(); 7836 r.setStart(node, offset); 7837 r.setEnd(node, offset); 7838 t.setRng(r); 7839 t.collapse(false); 7840 }, 7841 getContent : function(s) { 7842 var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n; 7843 7844 s = s || {}; 7845 wb = wa = ''; 7846 s.get = true; 7847 s.format = s.format || 'html'; 7848 s.forced_root_block = ''; 7849 t.onBeforeGetContent.dispatch(t, s); 7850 7851 if (s.format == 'text') 7852 return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : '')); 7853 7854 if (r.cloneContents) { 7855 n = r.cloneContents(); 7856 7857 if (n) 7858 e.appendChild(n); 7859 } else if (is(r.item) || is(r.htmlText)) { 7860 // IE will produce invalid markup if elements are present that 7861 // it doesn't understand like custom elements or HTML5 elements. 7862 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 7863 e.innerHTML = '<br>' + (r.item ? r.item(0).outerHTML : r.htmlText); 7864 e.removeChild(e.firstChild); 7865 } else 7866 e.innerHTML = r.toString(); 7867 7868 // Keep whitespace before and after 7869 if (/^\s/.test(e.innerHTML)) 7870 wb = ' '; 7871 7872 if (/\s+$/.test(e.innerHTML)) 7873 wa = ' '; 7874 7875 s.getInner = true; 7876 7877 s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa; 7878 t.onGetContent.dispatch(t, s); 7879 7880 return s.content; 7881 }, 7882 7883 setContent : function(content, args) { 7884 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 7885 7886 args = args || {format : 'html'}; 7887 args.set = true; 7888 content = args.content = content; 7889 7890 // Dispatch before set content event 7891 if (!args.no_events) 7892 self.onBeforeSetContent.dispatch(self, args); 7893 7894 content = args.content; 7895 7896 if (rng.insertNode) { 7897 // Make caret marker since insertNode places the caret in the beginning of text after insert 7898 content += '<span id="__caret">_</span>'; 7899 7900 // Delete and insert new node 7901 if (rng.startContainer == doc && rng.endContainer == doc) { 7902 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 7903 doc.body.innerHTML = content; 7904 } else { 7905 rng.deleteContents(); 7906 7907 if (doc.body.childNodes.length === 0) { 7908 doc.body.innerHTML = content; 7909 } else { 7910 // createContextualFragment doesn't exists in IE 9 DOMRanges 7911 if (rng.createContextualFragment) { 7912 rng.insertNode(rng.createContextualFragment(content)); 7913 } else { 7914 // Fake createContextualFragment call in IE 9 7915 frag = doc.createDocumentFragment(); 7916 temp = doc.createElement('div'); 7917 7918 frag.appendChild(temp); 7919 temp.outerHTML = content; 7920 7921 rng.insertNode(frag); 7922 } 7923 } 7924 } 7925 7926 // Move to caret marker 7927 caretNode = self.dom.get('__caret'); 7928 7929 // Make sure we wrap it compleatly, Opera fails with a simple select call 7930 rng = doc.createRange(); 7931 rng.setStartBefore(caretNode); 7932 rng.setEndBefore(caretNode); 7933 self.setRng(rng); 7934 7935 // Remove the caret position 7936 self.dom.remove('__caret'); 7937 7938 try { 7939 self.setRng(rng); 7940 } catch (ex) { 7941 // Might fail on Opera for some odd reason 7942 } 7943 } else { 7944 if (rng.item) { 7945 // Delete content and get caret text selection 7946 doc.execCommand('Delete', false, null); 7947 rng = self.getRng(); 7948 } 7949 7950 // Explorer removes spaces from the beginning of pasted contents 7951 if (/^\s+/.test(content)) { 7952 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 7953 self.dom.remove('__mce_tmp'); 7954 } else 7955 rng.pasteHTML(content); 7956 } 7957 7958 // Dispatch set content event 7959 if (!args.no_events) 7960 self.onSetContent.dispatch(self, args); 7961 }, 7962 7963 getStart : function() { 7964 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 7965 7966 if (rng.duplicate || rng.item) { 7967 // Control selection, return first item 7968 if (rng.item) 7969 return rng.item(0); 7970 7971 // Get start element 7972 checkRng = rng.duplicate(); 7973 checkRng.collapse(1); 7974 startElement = checkRng.parentElement(); 7975 if (startElement.ownerDocument !== self.dom.doc) { 7976 startElement = self.dom.getRoot(); 7977 } 7978 7979 // Check if range parent is inside the start element, then return the inner parent element 7980 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 7981 parentElement = node = rng.parentElement(); 7982 while (node = node.parentNode) { 7983 if (node == startElement) { 7984 startElement = parentElement; 7985 break; 7986 } 7987 } 7988 7989 return startElement; 7990 } else { 7991 startElement = rng.startContainer; 7992 7993 if (startElement.nodeType == 1 && startElement.hasChildNodes()) 7994 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 7995 7996 if (startElement && startElement.nodeType == 3) 7997 return startElement.parentNode; 7998 7999 return startElement; 8000 } 8001 }, 8002 8003 getEnd : function() { 8004 var self = this, rng = self.getRng(), endElement, endOffset; 8005 8006 if (rng.duplicate || rng.item) { 8007 if (rng.item) 8008 return rng.item(0); 8009 8010 rng = rng.duplicate(); 8011 rng.collapse(0); 8012 endElement = rng.parentElement(); 8013 if (endElement.ownerDocument !== self.dom.doc) { 8014 endElement = self.dom.getRoot(); 8015 } 8016 8017 if (endElement && endElement.nodeName == 'BODY') 8018 return endElement.lastChild || endElement; 8019 8020 return endElement; 8021 } else { 8022 endElement = rng.endContainer; 8023 endOffset = rng.endOffset; 8024 8025 if (endElement.nodeType == 1 && endElement.hasChildNodes()) 8026 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 8027 8028 if (endElement && endElement.nodeType == 3) 8029 return endElement.parentNode; 8030 8031 return endElement; 8032 } 8033 }, 8034 8035 getBookmark : function(type, normalized) { 8036 var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles; 8037 8038 function findIndex(name, element) { 8039 var index = 0; 8040 8041 each(dom.select(name), function(node, i) { 8042 if (node == element) 8043 index = i; 8044 }); 8045 8046 return index; 8047 }; 8048 8049 function normalizeTableCellSelection(rng) { 8050 function moveEndPoint(start) { 8051 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 8052 8053 container = rng[prefix + 'Container']; 8054 offset = rng[prefix + 'Offset']; 8055 8056 if (container.nodeType == 1 && container.nodeName == "TR") { 8057 childNodes = container.childNodes; 8058 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 8059 if (container) { 8060 offset = start ? 0 : container.childNodes.length; 8061 rng['set' + (start ? 'Start' : 'End')](container, offset); 8062 } 8063 } 8064 }; 8065 8066 moveEndPoint(true); 8067 moveEndPoint(); 8068 8069 return rng; 8070 }; 8071 8072 function getLocation() { 8073 var rng = t.getRng(true), root = dom.getRoot(), bookmark = {}; 8074 8075 function getPoint(rng, start) { 8076 var container = rng[start ? 'startContainer' : 'endContainer'], 8077 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 8078 8079 if (container.nodeType == 3) { 8080 if (normalized) { 8081 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) 8082 offset += node.nodeValue.length; 8083 } 8084 8085 point.push(offset); 8086 } else { 8087 childNodes = container.childNodes; 8088 8089 if (offset >= childNodes.length && childNodes.length) { 8090 after = 1; 8091 offset = Math.max(0, childNodes.length - 1); 8092 } 8093 8094 point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after); 8095 } 8096 8097 for (; container && container != root; container = container.parentNode) 8098 point.push(t.dom.nodeIndex(container, normalized)); 8099 8100 return point; 8101 }; 8102 8103 bookmark.start = getPoint(rng, true); 8104 8105 if (!t.isCollapsed()) 8106 bookmark.end = getPoint(rng); 8107 8108 return bookmark; 8109 }; 8110 8111 if (type == 2) { 8112 if (t.tridentSel) 8113 return t.tridentSel.getBookmark(type); 8114 8115 return getLocation(); 8116 } 8117 8118 // Handle simple range 8119 if (type) 8120 return {rng : t.getRng()}; 8121 8122 rng = t.getRng(); 8123 id = dom.uniqueId(); 8124 collapsed = tinyMCE.activeEditor.selection.isCollapsed(); 8125 styles = 'overflow:hidden;line-height:0px'; 8126 8127 // Explorer method 8128 if (rng.duplicate || rng.item) { 8129 // Text selection 8130 if (!rng.item) { 8131 rng2 = rng.duplicate(); 8132 8133 try { 8134 // Insert start marker 8135 rng.collapse(); 8136 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 8137 8138 // Insert end marker 8139 if (!collapsed) { 8140 rng2.collapse(false); 8141 8142 // Detect the empty space after block elements in IE and move the end back one character <p></p>] becomes <p>]</p> 8143 rng.moveToElementText(rng2.parentElement()); 8144 if (rng.compareEndPoints('StartToEnd', rng2) === 0) 8145 rng2.move('character', -1); 8146 8147 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 8148 } 8149 } catch (ex) { 8150 // IE might throw unspecified error so lets ignore it 8151 return null; 8152 } 8153 } else { 8154 // Control selection 8155 element = rng.item(0); 8156 name = element.nodeName; 8157 8158 return {name : name, index : findIndex(name, element)}; 8159 } 8160 } else { 8161 element = t.getNode(); 8162 name = element.nodeName; 8163 if (name == 'IMG') 8164 return {name : name, index : findIndex(name, element)}; 8165 8166 // W3C method 8167 rng2 = normalizeTableCellSelection(rng.cloneRange()); 8168 8169 // Insert end marker 8170 if (!collapsed) { 8171 rng2.collapse(false); 8172 rng2.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_end', style : styles}, chr)); 8173 } 8174 8175 rng = normalizeTableCellSelection(rng); 8176 rng.collapse(true); 8177 rng.insertNode(dom.create('span', {'data-mce-type' : "bookmark", id : id + '_start', style : styles}, chr)); 8178 } 8179 8180 t.moveToBookmark({id : id, keep : 1}); 8181 8182 return {id : id}; 8183 }, 8184 8185 moveToBookmark : function(bookmark) { 8186 var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset; 8187 8188 function setEndPoint(start) { 8189 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 8190 8191 if (point) { 8192 offset = point[0]; 8193 8194 // Find container node 8195 for (node = root, i = point.length - 1; i >= 1; i--) { 8196 children = node.childNodes; 8197 8198 if (point[i] > children.length - 1) 8199 return; 8200 8201 node = children[point[i]]; 8202 } 8203 8204 // Move text offset to best suitable location 8205 if (node.nodeType === 3) 8206 offset = Math.min(point[0], node.nodeValue.length); 8207 8208 // Move element offset to best suitable location 8209 if (node.nodeType === 1) 8210 offset = Math.min(point[0], node.childNodes.length); 8211 8212 // Set offset within container node 8213 if (start) 8214 rng.setStart(node, offset); 8215 else 8216 rng.setEnd(node, offset); 8217 } 8218 8219 return true; 8220 }; 8221 8222 function restoreEndPoint(suffix) { 8223 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 8224 8225 if (marker) { 8226 node = marker.parentNode; 8227 8228 if (suffix == 'start') { 8229 if (!keep) { 8230 idx = dom.nodeIndex(marker); 8231 } else { 8232 node = marker.firstChild; 8233 idx = 1; 8234 } 8235 8236 startContainer = endContainer = node; 8237 startOffset = endOffset = idx; 8238 } else { 8239 if (!keep) { 8240 idx = dom.nodeIndex(marker); 8241 } else { 8242 node = marker.firstChild; 8243 idx = 1; 8244 } 8245 8246 endContainer = node; 8247 endOffset = idx; 8248 } 8249 8250 if (!keep) { 8251 prev = marker.previousSibling; 8252 next = marker.nextSibling; 8253 8254 // Remove all marker text nodes 8255 each(tinymce.grep(marker.childNodes), function(node) { 8256 if (node.nodeType == 3) 8257 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 8258 }); 8259 8260 // Remove marker but keep children if for example contents where inserted into the marker 8261 // Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature 8262 while (marker = dom.get(bookmark.id + '_' + suffix)) 8263 dom.remove(marker, 1); 8264 8265 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 8266 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market isn't worth the effort. Sorry, Opera but it's just a fact 8267 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !tinymce.isOpera) { 8268 idx = prev.nodeValue.length; 8269 prev.appendData(next.nodeValue); 8270 dom.remove(next); 8271 8272 if (suffix == 'start') { 8273 startContainer = endContainer = prev; 8274 startOffset = endOffset = idx; 8275 } else { 8276 endContainer = prev; 8277 endOffset = idx; 8278 } 8279 } 8280 } 8281 } 8282 }; 8283 8284 function addBogus(node) { 8285 // Adds a bogus BR element for empty block elements 8286 if (dom.isBlock(node) && !node.innerHTML && !isIE) 8287 node.innerHTML = '<br data-mce-bogus="1" />'; 8288 8289 return node; 8290 }; 8291 8292 if (bookmark) { 8293 if (bookmark.start) { 8294 rng = dom.createRng(); 8295 root = dom.getRoot(); 8296 8297 if (t.tridentSel) 8298 return t.tridentSel.moveToBookmark(bookmark); 8299 8300 if (setEndPoint(true) && setEndPoint()) { 8301 t.setRng(rng); 8302 } 8303 } else if (bookmark.id) { 8304 // Restore start/end points 8305 restoreEndPoint('start'); 8306 restoreEndPoint('end'); 8307 8308 if (startContainer) { 8309 rng = dom.createRng(); 8310 rng.setStart(addBogus(startContainer), startOffset); 8311 rng.setEnd(addBogus(endContainer), endOffset); 8312 t.setRng(rng); 8313 } 8314 } else if (bookmark.name) { 8315 t.select(dom.select(bookmark.name)[bookmark.index]); 8316 } else if (bookmark.rng) 8317 t.setRng(bookmark.rng); 8318 } 8319 }, 8320 8321 select : function(node, content) { 8322 var t = this, dom = t.dom, rng = dom.createRng(), idx; 8323 8324 function setPoint(node, start) { 8325 var walker = new TreeWalker(node, node); 8326 8327 do { 8328 // Text node 8329 if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length !== 0) { 8330 if (start) 8331 rng.setStart(node, 0); 8332 else 8333 rng.setEnd(node, node.nodeValue.length); 8334 8335 return; 8336 } 8337 8338 // BR element 8339 if (node.nodeName == 'BR') { 8340 if (start) 8341 rng.setStartBefore(node); 8342 else 8343 rng.setEndBefore(node); 8344 8345 return; 8346 } 8347 } while (node = (start ? walker.next() : walker.prev())); 8348 }; 8349 8350 if (node) { 8351 idx = dom.nodeIndex(node); 8352 rng.setStart(node.parentNode, idx); 8353 rng.setEnd(node.parentNode, idx + 1); 8354 8355 // Find first/last text node or BR element 8356 if (content) { 8357 setPoint(node, 1); 8358 setPoint(node); 8359 } 8360 8361 t.setRng(rng); 8362 } 8363 8364 return node; 8365 }, 8366 8367 isCollapsed : function() { 8368 var t = this, r = t.getRng(), s = t.getSel(); 8369 8370 if (!r || r.item) 8371 return false; 8372 8373 if (r.compareEndPoints) 8374 return r.compareEndPoints('StartToEnd', r) === 0; 8375 8376 return !s || r.collapsed; 8377 }, 8378 8379 collapse : function(to_start) { 8380 var self = this, rng = self.getRng(), node; 8381 8382 // Control range on IE 8383 if (rng.item) { 8384 node = rng.item(0); 8385 rng = self.win.document.body.createTextRange(); 8386 rng.moveToElementText(node); 8387 } 8388 8389 rng.collapse(!!to_start); 8390 self.setRng(rng); 8391 }, 8392 8393 getSel : function() { 8394 var t = this, w = this.win; 8395 8396 return w.getSelection ? w.getSelection() : w.document.selection; 8397 }, 8398 8399 getRng : function(w3c) { 8400 var self = this, selection, rng, elm, doc = self.win.document; 8401 8402 // Found tridentSel object then we need to use that one 8403 if (w3c && self.tridentSel) { 8404 return self.tridentSel.getRangeAt(0); 8405 } 8406 8407 try { 8408 if (selection = self.getSel()) { 8409 rng = selection.rangeCount > 0 ? selection.getRangeAt(0) : (selection.createRange ? selection.createRange() : doc.createRange()); 8410 } 8411 } catch (ex) { 8412 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 8413 } 8414 8415 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 8416 if (tinymce.isIE && rng && rng.setStart && doc.selection.createRange().item) { 8417 elm = doc.selection.createRange().item(0); 8418 rng = doc.createRange(); 8419 rng.setStartBefore(elm); 8420 rng.setEndAfter(elm); 8421 } 8422 8423 // No range found then create an empty one 8424 // This can occur when the editor is placed in a hidden container element on Gecko 8425 // Or on IE when there was an exception 8426 if (!rng) { 8427 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 8428 } 8429 8430 // If range is at start of document then move it to start of body 8431 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 8432 elm = self.dom.getRoot(); 8433 rng.setStart(elm, 0); 8434 rng.setEnd(elm, 0); 8435 } 8436 8437 if (self.selectedRange && self.explicitRange) { 8438 if (rng.compareBoundaryPoints(rng.START_TO_START, self.selectedRange) === 0 && rng.compareBoundaryPoints(rng.END_TO_END, self.selectedRange) === 0) { 8439 // Safari, Opera and Chrome only ever select text which causes the range to change. 8440 // This lets us use the originally set range if the selection hasn't been changed by the user. 8441 rng = self.explicitRange; 8442 } else { 8443 self.selectedRange = null; 8444 self.explicitRange = null; 8445 } 8446 } 8447 8448 return rng; 8449 }, 8450 8451 setRng : function(r, forward) { 8452 var s, t = this; 8453 8454 if (!t.tridentSel) { 8455 s = t.getSel(); 8456 8457 if (s) { 8458 t.explicitRange = r; 8459 8460 try { 8461 s.removeAllRanges(); 8462 } catch (ex) { 8463 // IE9 might throw errors here don't know why 8464 } 8465 8466 s.addRange(r); 8467 8468 // Forward is set to false and we have an extend function 8469 if (forward === false && s.extend) { 8470 s.collapse(r.endContainer, r.endOffset); 8471 s.extend(r.startContainer, r.startOffset); 8472 } 8473 8474 // adding range isn't always successful so we need to check range count otherwise an exception can occur 8475 t.selectedRange = s.rangeCount > 0 ? s.getRangeAt(0) : null; 8476 } 8477 } else { 8478 // Is W3C Range 8479 if (r.cloneRange) { 8480 try { 8481 t.tridentSel.addRange(r); 8482 return; 8483 } catch (ex) { 8484 //IE9 throws an error here if called before selection is placed in the editor 8485 } 8486 } 8487 8488 // Is IE specific range 8489 try { 8490 r.select(); 8491 } catch (ex) { 8492 // Needed for some odd IE bug #1843306 8493 } 8494 } 8495 }, 8496 8497 setNode : function(n) { 8498 var t = this; 8499 8500 t.setContent(t.dom.getOuterHTML(n)); 8501 8502 return n; 8503 }, 8504 8505 getNode : function() { 8506 var t = this, rng = t.getRng(), sel = t.getSel(), elm, start = rng.startContainer, end = rng.endContainer; 8507 8508 function skipEmptyTextNodes(n, forwards) { 8509 var orig = n; 8510 while (n && n.nodeType === 3 && n.length === 0) { 8511 n = forwards ? n.nextSibling : n.previousSibling; 8512 } 8513 return n || orig; 8514 }; 8515 8516 // Range maybe lost after the editor is made visible again 8517 if (!rng) 8518 return t.dom.getRoot(); 8519 8520 if (rng.setStart) { 8521 elm = rng.commonAncestorContainer; 8522 8523 // Handle selection a image or other control like element such as anchors 8524 if (!rng.collapsed) { 8525 if (rng.startContainer == rng.endContainer) { 8526 if (rng.endOffset - rng.startOffset < 2) { 8527 if (rng.startContainer.hasChildNodes()) 8528 elm = rng.startContainer.childNodes[rng.startOffset]; 8529 } 8530 } 8531 8532 // If the anchor node is a element instead of a text node then return this element 8533 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 8534 // return sel.anchorNode.childNodes[sel.anchorOffset]; 8535 8536 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 8537 // This happens when you double click an underlined word in FireFox. 8538 if (start.nodeType === 3 && end.nodeType === 3) { 8539 if (start.length === rng.startOffset) { 8540 start = skipEmptyTextNodes(start.nextSibling, true); 8541 } else { 8542 start = start.parentNode; 8543 } 8544 if (rng.endOffset === 0) { 8545 end = skipEmptyTextNodes(end.previousSibling, false); 8546 } else { 8547 end = end.parentNode; 8548 } 8549 8550 if (start && start === end) 8551 return start; 8552 } 8553 } 8554 8555 if (elm && elm.nodeType == 3) 8556 return elm.parentNode; 8557 8558 return elm; 8559 } 8560 8561 return rng.item ? rng.item(0) : rng.parentElement(); 8562 }, 8563 8564 getSelectedBlocks : function(st, en) { 8565 var t = this, dom = t.dom, sb, eb, n, bl = []; 8566 8567 sb = dom.getParent(st || t.getStart(), dom.isBlock); 8568 eb = dom.getParent(en || t.getEnd(), dom.isBlock); 8569 8570 if (sb) 8571 bl.push(sb); 8572 8573 if (sb && eb && sb != eb) { 8574 n = sb; 8575 8576 var walker = new TreeWalker(sb, dom.getRoot()); 8577 while ((n = walker.next()) && n != eb) { 8578 if (dom.isBlock(n)) 8579 bl.push(n); 8580 } 8581 } 8582 8583 if (eb && sb != eb) 8584 bl.push(eb); 8585 8586 return bl; 8587 }, 8588 8589 isForward: function(){ 8590 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 8591 8592 // No support for selection direction then always return true 8593 if (!sel || sel.anchorNode == null || sel.focusNode == null) { 8594 return true; 8595 } 8596 8597 anchorRange = dom.createRng(); 8598 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 8599 anchorRange.collapse(true); 8600 8601 focusRange = dom.createRng(); 8602 focusRange.setStart(sel.focusNode, sel.focusOffset); 8603 focusRange.collapse(true); 8604 8605 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 8606 }, 8607 8608 normalize : function() { 8609 var self = this, rng, normalized, collapsed, node, sibling; 8610 8611 function normalizeEndPoint(start) { 8612 var container, offset, walker, dom = self.dom, body = dom.getRoot(), node, nonEmptyElementsMap, nodeName; 8613 8614 function hasBrBeforeAfter(node, left) { 8615 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 8616 8617 while (node = walker[left ? 'prev' : 'next']()) { 8618 if (node.nodeName === "BR") { 8619 return true; 8620 } 8621 } 8622 }; 8623 8624 // Walks the dom left/right to find a suitable text node to move the endpoint into 8625 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 8626 function findTextNodeRelative(left, startNode) { 8627 var walker, lastInlineElement; 8628 8629 startNode = startNode || container; 8630 walker = new TreeWalker(startNode, dom.getParent(startNode.parentNode, dom.isBlock) || body); 8631 8632 // Walk left until we hit a text node we can move to or a block/br/img 8633 while (node = walker[left ? 'prev' : 'next']()) { 8634 // Found text node that has a length 8635 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8636 container = node; 8637 offset = left ? node.nodeValue.length : 0; 8638 normalized = true; 8639 return; 8640 } 8641 8642 // Break if we find a block or a BR/IMG/INPUT etc 8643 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8644 return; 8645 } 8646 8647 lastInlineElement = node; 8648 } 8649 8650 // Only fetch the last inline element when in caret mode for now 8651 if (collapsed && lastInlineElement) { 8652 container = lastInlineElement; 8653 normalized = true; 8654 offset = 0; 8655 } 8656 }; 8657 8658 container = rng[(start ? 'start' : 'end') + 'Container']; 8659 offset = rng[(start ? 'start' : 'end') + 'Offset']; 8660 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 8661 8662 // If the container is a document move it to the body element 8663 if (container.nodeType === 9) { 8664 container = dom.getRoot(); 8665 offset = 0; 8666 } 8667 8668 // If the container is body try move it into the closest text node or position 8669 if (container === body) { 8670 // If start is before/after a image, table etc 8671 if (start) { 8672 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 8673 if (node) { 8674 nodeName = node.nodeName.toLowerCase(); 8675 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 8676 return; 8677 } 8678 } 8679 } 8680 8681 // Resolve the index 8682 if (container.hasChildNodes()) { 8683 container = container.childNodes[Math.min(!start && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1)]; 8684 offset = 0; 8685 8686 // Don't walk into elements that doesn't have any child nodes like a IMG 8687 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 8688 // Walk the DOM to find a text node to place the caret at or a BR 8689 node = container; 8690 walker = new TreeWalker(container, body); 8691 8692 do { 8693 // Found a text node use that position 8694 if (node.nodeType === 3 && node.nodeValue.length > 0) { 8695 offset = start ? 0 : node.nodeValue.length; 8696 container = node; 8697 normalized = true; 8698 break; 8699 } 8700 8701 // Found a BR/IMG element that we can place the caret before 8702 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 8703 offset = dom.nodeIndex(node); 8704 container = node.parentNode; 8705 8706 // Put caret after image when moving the end point 8707 if (node.nodeName == "IMG" && !start) { 8708 offset++; 8709 } 8710 8711 normalized = true; 8712 break; 8713 } 8714 } while (node = (start ? walker.next() : walker.prev())); 8715 } 8716 } 8717 } 8718 8719 // Lean the caret to the left if possible 8720 if (collapsed) { 8721 // So this: <b>x</b><i>|x</i> 8722 // Becomes: <b>x|</b><i>x</i> 8723 // Seems that only gecko has issues with this 8724 if (container.nodeType === 3 && offset === 0) { 8725 findTextNodeRelative(true); 8726 } 8727 8728 // Lean left into empty inline elements when the caret is before a BR 8729 // So this: <i><b></b><i>|<br></i> 8730 // Becomes: <i><b>|</b><i><br></i> 8731 // Seems that only gecko has issues with this 8732 if (container.nodeType === 1) { 8733 node = container.childNodes[offset]; 8734 if(node && node.nodeName === 'BR' && !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 8735 findTextNodeRelative(true, container.childNodes[offset]); 8736 } 8737 } 8738 } 8739 8740 // Lean the start of the selection right if possible 8741 // So this: x[<b>x]</b> 8742 // Becomes: x<b>[x]</b> 8743 if (start && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 8744 findTextNodeRelative(false); 8745 } 8746 8747 // Set endpoint if it was normalized 8748 if (normalized) 8749 rng['set' + (start ? 'Start' : 'End')](container, offset); 8750 }; 8751 8752 // Normalize only on non IE browsers for now 8753 if (tinymce.isIE) 8754 return; 8755 8756 rng = self.getRng(); 8757 collapsed = rng.collapsed; 8758 8759 // Normalize the end points 8760 normalizeEndPoint(true); 8761 8762 if (!collapsed) 8763 normalizeEndPoint(); 8764 8765 // Set the selection if it was normalized 8766 if (normalized) { 8767 // If it was collapsed then make sure it still is 8768 if (collapsed) { 8769 rng.collapse(true); 8770 } 8771 8772 //console.log(self.dom.dumpRng(rng)); 8773 self.setRng(rng, self.isForward()); 8774 } 8775 }, 8776 8777 selectorChanged: function(selector, callback) { 8778 var self = this, currentSelectors; 8779 8780 if (!self.selectorChangedData) { 8781 self.selectorChangedData = {}; 8782 currentSelectors = {}; 8783 8784 self.editor.onNodeChange.addToTop(function(ed, cm, node) { 8785 var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 8786 8787 // Check for new matching selectors 8788 each(self.selectorChangedData, function(callbacks, selector) { 8789 each(parents, function(node) { 8790 if (dom.is(node, selector)) { 8791 if (!currentSelectors[selector]) { 8792 // Execute callbacks 8793 each(callbacks, function(callback) { 8794 callback(true, {node: node, selector: selector, parents: parents}); 8795 }); 8796 8797 currentSelectors[selector] = callbacks; 8798 } 8799 8800 matchedSelectors[selector] = callbacks; 8801 return false; 8802 } 8803 }); 8804 }); 8805 8806 // Check if current selectors still match 8807 each(currentSelectors, function(callbacks, selector) { 8808 if (!matchedSelectors[selector]) { 8809 delete currentSelectors[selector]; 8810 8811 each(callbacks, function(callback) { 8812 callback(false, {node: node, selector: selector, parents: parents}); 8813 }); 8814 } 8815 }); 8816 }); 8817 } 8818 8819 // Add selector listeners 8820 if (!self.selectorChangedData[selector]) { 8821 self.selectorChangedData[selector] = []; 8822 } 8823 8824 self.selectorChangedData[selector].push(callback); 8825 8826 return self; 8827 }, 8828 8829 destroy : function(manual) { 8830 var self = this; 8831 8832 self.win = null; 8833 8834 // Manual destroy then remove unload handler 8835 if (!manual) 8836 tinymce.removeUnload(self.destroy); 8837 }, 8838 8839 // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode 8840 _fixIESelection : function() { 8841 var dom = this.dom, doc = dom.doc, body = doc.body, started, startRng, htmlElm; 8842 8843 // Return range from point or null if it failed 8844 function rngFromPoint(x, y) { 8845 var rng = body.createTextRange(); 8846 8847 try { 8848 rng.moveToPoint(x, y); 8849 } catch (ex) { 8850 // IE sometimes throws and exception, so lets just ignore it 8851 rng = null; 8852 } 8853 8854 return rng; 8855 }; 8856 8857 // Fires while the selection is changing 8858 function selectionChange(e) { 8859 var pointRng; 8860 8861 // Check if the button is down or not 8862 if (e.button) { 8863 // Create range from mouse position 8864 pointRng = rngFromPoint(e.x, e.y); 8865 8866 if (pointRng) { 8867 // Check if pointRange is before/after selection then change the endPoint 8868 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) 8869 pointRng.setEndPoint('StartToStart', startRng); 8870 else 8871 pointRng.setEndPoint('EndToEnd', startRng); 8872 8873 pointRng.select(); 8874 } 8875 } else 8876 endSelection(); 8877 } 8878 8879 // Removes listeners 8880 function endSelection() { 8881 var rng = doc.selection.createRange(); 8882 8883 // If the range is collapsed then use the last start range 8884 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) 8885 startRng.select(); 8886 8887 dom.unbind(doc, 'mouseup', endSelection); 8888 dom.unbind(doc, 'mousemove', selectionChange); 8889 startRng = started = 0; 8890 }; 8891 8892 // Make HTML element unselectable since we are going to handle selection by hand 8893 doc.documentElement.unselectable = true; 8894 8895 // Detect when user selects outside BODY 8896 dom.bind(doc, ['mousedown', 'contextmenu'], function(e) { 8897 if (e.target.nodeName === 'HTML') { 8898 if (started) 8899 endSelection(); 8900 8901 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 8902 htmlElm = doc.documentElement; 8903 if (htmlElm.scrollHeight > htmlElm.clientHeight) 8904 return; 8905 8906 started = 1; 8907 // Setup start position 8908 startRng = rngFromPoint(e.x, e.y); 8909 if (startRng) { 8910 // Listen for selection change events 8911 dom.bind(doc, 'mouseup', endSelection); 8912 dom.bind(doc, 'mousemove', selectionChange); 8913 8914 dom.win.focus(); 8915 startRng.select(); 8916 } 8917 } 8918 }); 8919 } 8920 }); 8921 })(tinymce); 8922 8923 (function(tinymce) { 8924 tinymce.dom.Serializer = function(settings, dom, schema) { 8925 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser; 8926 8927 // Support the old apply_source_formatting option 8928 if (!settings.apply_source_formatting) 8929 settings.indent = false; 8930 8931 // Default DOM and Schema if they are undefined 8932 dom = dom || tinymce.DOM; 8933 schema = schema || new tinymce.html.Schema(settings); 8934 settings.entity_encoding = settings.entity_encoding || 'named'; 8935 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 8936 8937 onPreProcess = new tinymce.util.Dispatcher(self); 8938 8939 onPostProcess = new tinymce.util.Dispatcher(self); 8940 8941 htmlParser = new tinymce.html.DomParser(settings, schema); 8942 8943 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 8944 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 8945 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 8946 8947 while (i--) { 8948 node = nodes[i]; 8949 8950 value = node.attributes.map[internalName]; 8951 if (value !== undef) { 8952 // Set external name to internal value and remove internal 8953 node.attr(name, value.length > 0 ? value : null); 8954 node.attr(internalName, null); 8955 } else { 8956 // No internal attribute found then convert the value we have in the DOM 8957 value = node.attributes.map[name]; 8958 8959 if (name === "style") 8960 value = dom.serializeStyle(dom.parseStyle(value), node.name); 8961 else if (urlConverter) 8962 value = urlConverter.call(urlConverterScope, value, name, node.name); 8963 8964 node.attr(name, value.length > 0 ? value : null); 8965 } 8966 } 8967 }); 8968 8969 // Remove internal classes mceItem<..> or mceSelected 8970 htmlParser.addAttributeFilter('class', function(nodes, name) { 8971 var i = nodes.length, node, value; 8972 8973 while (i--) { 8974 node = nodes[i]; 8975 value = node.attr('class').replace(/(?:^|\s)mce(Item\w+|Selected)(?!\S)/g, ''); 8976 node.attr('class', value.length > 0 ? value : null); 8977 } 8978 }); 8979 8980 // Remove bookmark elements 8981 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 8982 var i = nodes.length, node; 8983 8984 while (i--) { 8985 node = nodes[i]; 8986 8987 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) 8988 node.remove(); 8989 } 8990 }); 8991 8992 // Remove expando attributes 8993 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name, args) { 8994 var i = nodes.length; 8995 8996 while (i--) { 8997 nodes[i].attr(name, null); 8998 } 8999 }); 9000 9001 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 9002 htmlParser.addNodeFilter('script,style', function(nodes, name) { 9003 var i = nodes.length, node, value; 9004 9005 function trim(value) { 9006 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 9007 .replace(/^[\r\n]*|[\r\n]*$/g, '') 9008 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 9009 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 9010 }; 9011 9012 while (i--) { 9013 node = nodes[i]; 9014 value = node.firstChild ? node.firstChild.value : ''; 9015 9016 if (name === "script") { 9017 // Remove mce- prefix from script elements 9018 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, '')); 9019 9020 if (value.length > 0) 9021 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 9022 } else { 9023 if (value.length > 0) 9024 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 9025 } 9026 } 9027 }); 9028 9029 // Convert comments to cdata and handle protected comments 9030 htmlParser.addNodeFilter('#comment', function(nodes, name) { 9031 var i = nodes.length, node; 9032 9033 while (i--) { 9034 node = nodes[i]; 9035 9036 if (node.value.indexOf('[CDATA[') === 0) { 9037 node.name = '#cdata'; 9038 node.type = 4; 9039 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 9040 } else if (node.value.indexOf('mce:protected ') === 0) { 9041 node.name = "#text"; 9042 node.type = 3; 9043 node.raw = true; 9044 node.value = unescape(node.value).substr(14); 9045 } 9046 } 9047 }); 9048 9049 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 9050 var i = nodes.length, node; 9051 9052 while (i--) { 9053 node = nodes[i]; 9054 if (node.type === 7) 9055 node.remove(); 9056 else if (node.type === 1) { 9057 if (name === "input" && !("type" in node.attributes.map)) 9058 node.attr('type', 'text'); 9059 } 9060 } 9061 }); 9062 9063 // Fix list elements, TODO: Replace this later 9064 if (settings.fix_list_elements) { 9065 htmlParser.addNodeFilter('ul,ol', function(nodes, name) { 9066 var i = nodes.length, node, parentNode; 9067 9068 while (i--) { 9069 node = nodes[i]; 9070 parentNode = node.parent; 9071 9072 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 9073 if (node.prev && node.prev.name === 'li') { 9074 node.prev.append(node); 9075 } 9076 } 9077 } 9078 }); 9079 } 9080 9081 // Remove internal data attributes 9082 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) { 9083 var i = nodes.length; 9084 9085 while (i--) { 9086 nodes[i].attr(name, null); 9087 } 9088 }); 9089 9090 // Return public methods 9091 return { 9092 schema : schema, 9093 9094 addNodeFilter : htmlParser.addNodeFilter, 9095 9096 addAttributeFilter : htmlParser.addAttributeFilter, 9097 9098 onPreProcess : onPreProcess, 9099 9100 onPostProcess : onPostProcess, 9101 9102 serialize : function(node, args) { 9103 var impl, doc, oldDoc, htmlSerializer, content; 9104 9105 // Explorer won't clone contents of script and style and the 9106 // selected index of select elements are cleared on a clone operation. 9107 if (isIE && dom.select('script,style,select,map').length > 0) { 9108 content = node.innerHTML; 9109 node = node.cloneNode(false); 9110 dom.setHTML(node, content); 9111 } else 9112 node = node.cloneNode(true); 9113 9114 // Nodes needs to be attached to something in WebKit/Opera 9115 // Older builds of Opera crashes if you attach the node to an document created dynamically 9116 // and since we can't feature detect a crash we need to sniff the acutal build number 9117 // This fix will make DOM ranges and make Sizzle happy! 9118 impl = node.ownerDocument.implementation; 9119 if (impl.createHTMLDocument) { 9120 // Create an empty HTML document 9121 doc = impl.createHTMLDocument(""); 9122 9123 // Add the element or it's children if it's a body element to the new document 9124 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 9125 doc.body.appendChild(doc.importNode(node, true)); 9126 }); 9127 9128 // Grab first child or body element for serialization 9129 if (node.nodeName != 'BODY') 9130 node = doc.body.firstChild; 9131 else 9132 node = doc.body; 9133 9134 // set the new document in DOMUtils so createElement etc works 9135 oldDoc = dom.doc; 9136 dom.doc = doc; 9137 } 9138 9139 args = args || {}; 9140 args.format = args.format || 'html'; 9141 9142 // Pre process 9143 if (!args.no_events) { 9144 args.node = node; 9145 onPreProcess.dispatch(self, args); 9146 } 9147 9148 // Setup serializer 9149 htmlSerializer = new tinymce.html.Serializer(settings, schema); 9150 9151 // Parse and serialize HTML 9152 args.content = htmlSerializer.serialize( 9153 htmlParser.parse(tinymce.trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 9154 ); 9155 9156 // Replace all BOM characters for now until we can find a better solution 9157 if (!args.cleanup) 9158 args.content = args.content.replace(/\uFEFF|\u200B/g, ''); 9159 9160 // Post process 9161 if (!args.no_events) 9162 onPostProcess.dispatch(self, args); 9163 9164 // Restore the old document if it was changed 9165 if (oldDoc) 9166 dom.doc = oldDoc; 9167 9168 args.node = null; 9169 9170 return args.content; 9171 }, 9172 9173 addRules : function(rules) { 9174 schema.addValidElements(rules); 9175 }, 9176 9177 setRules : function(rules) { 9178 schema.setValidElements(rules); 9179 } 9180 }; 9181 }; 9182 })(tinymce); 9183 (function(tinymce) { 9184 tinymce.dom.ScriptLoader = function(settings) { 9185 var QUEUED = 0, 9186 LOADING = 1, 9187 LOADED = 2, 9188 states = {}, 9189 queue = [], 9190 scriptLoadedCallbacks = {}, 9191 queueLoadedCallbacks = [], 9192 loading = 0, 9193 undef; 9194 9195 function loadScript(url, callback) { 9196 var t = this, dom = tinymce.DOM, elm, uri, loc, id; 9197 9198 // Execute callback when script is loaded 9199 function done() { 9200 dom.remove(id); 9201 9202 if (elm) 9203 elm.onreadystatechange = elm.onload = elm = null; 9204 9205 callback(); 9206 }; 9207 9208 function error() { 9209 // Report the error so it's easier for people to spot loading errors 9210 if (typeof(console) !== "undefined" && console.log) 9211 console.log("Failed to load: " + url); 9212 9213 // We can't mark it as done if there is a load error since 9214 // A) We don't want to produce 404 errors on the server and 9215 // B) the onerror event won't fire on all browsers. 9216 // done(); 9217 }; 9218 9219 id = dom.uniqueId(); 9220 9221 if (tinymce.isIE6) { 9222 uri = new tinymce.util.URI(url); 9223 loc = location; 9224 9225 // If script is from same domain and we 9226 // use IE 6 then use XHR since it's more reliable 9227 if (uri.host == loc.hostname && uri.port == loc.port && (uri.protocol + ':') == loc.protocol && uri.protocol.toLowerCase() != 'file') { 9228 tinymce.util.XHR.send({ 9229 url : tinymce._addVer(uri.getURI()), 9230 success : function(content) { 9231 // Create new temp script element 9232 var script = dom.create('script', { 9233 type : 'text/javascript' 9234 }); 9235 9236 // Evaluate script in global scope 9237 script.text = content; 9238 document.getElementsByTagName('head')[0].appendChild(script); 9239 dom.remove(script); 9240 9241 done(); 9242 }, 9243 9244 error : error 9245 }); 9246 9247 return; 9248 } 9249 } 9250 9251 // Create new script element 9252 elm = document.createElement('script'); 9253 elm.id = id; 9254 elm.type = 'text/javascript'; 9255 elm.src = tinymce._addVer(url); 9256 9257 // Add onload listener for non IE browsers since IE9 9258 // fires onload event before the script is parsed and executed 9259 if (!tinymce.isIE) 9260 elm.onload = done; 9261 9262 // Add onerror event will get fired on some browsers but not all of them 9263 elm.onerror = error; 9264 9265 // Opera 9.60 doesn't seem to fire the onreadystate event at correctly 9266 if (!tinymce.isOpera) { 9267 elm.onreadystatechange = function() { 9268 var state = elm.readyState; 9269 9270 // Loaded state is passed on IE 6 however there 9271 // are known issues with this method but we can't use 9272 // XHR in a cross domain loading 9273 if (state == 'complete' || state == 'loaded') 9274 done(); 9275 }; 9276 } 9277 9278 // Most browsers support this feature so we report errors 9279 // for those at least to help users track their missing plugins etc 9280 // todo: Removed since it produced error if the document is unloaded by navigating away, re-add it as an option 9281 /*elm.onerror = function() { 9282 alert('Failed to load: ' + url); 9283 };*/ 9284 9285 // Add script to document 9286 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 9287 }; 9288 9289 this.isDone = function(url) { 9290 return states[url] == LOADED; 9291 }; 9292 9293 this.markDone = function(url) { 9294 states[url] = LOADED; 9295 }; 9296 9297 this.add = this.load = function(url, callback, scope) { 9298 var item, state = states[url]; 9299 9300 // Add url to load queue 9301 if (state == undef) { 9302 queue.push(url); 9303 states[url] = QUEUED; 9304 } 9305 9306 if (callback) { 9307 // Store away callback for later execution 9308 if (!scriptLoadedCallbacks[url]) 9309 scriptLoadedCallbacks[url] = []; 9310 9311 scriptLoadedCallbacks[url].push({ 9312 func : callback, 9313 scope : scope || this 9314 }); 9315 } 9316 }; 9317 9318 this.loadQueue = function(callback, scope) { 9319 this.loadScripts(queue, callback, scope); 9320 }; 9321 9322 this.loadScripts = function(scripts, callback, scope) { 9323 var loadScripts; 9324 9325 function execScriptLoadedCallbacks(url) { 9326 // Execute URL callback functions 9327 tinymce.each(scriptLoadedCallbacks[url], function(callback) { 9328 callback.func.call(callback.scope); 9329 }); 9330 9331 scriptLoadedCallbacks[url] = undef; 9332 }; 9333 9334 queueLoadedCallbacks.push({ 9335 func : callback, 9336 scope : scope || this 9337 }); 9338 9339 loadScripts = function() { 9340 var loadingScripts = tinymce.grep(scripts); 9341 9342 // Current scripts has been handled 9343 scripts.length = 0; 9344 9345 // Load scripts that needs to be loaded 9346 tinymce.each(loadingScripts, function(url) { 9347 // Script is already loaded then execute script callbacks directly 9348 if (states[url] == LOADED) { 9349 execScriptLoadedCallbacks(url); 9350 return; 9351 } 9352 9353 // Is script not loading then start loading it 9354 if (states[url] != LOADING) { 9355 states[url] = LOADING; 9356 loading++; 9357 9358 loadScript(url, function() { 9359 states[url] = LOADED; 9360 loading--; 9361 9362 execScriptLoadedCallbacks(url); 9363 9364 // Load more scripts if they where added by the recently loaded script 9365 loadScripts(); 9366 }); 9367 } 9368 }); 9369 9370 // No scripts are currently loading then execute all pending queue loaded callbacks 9371 if (!loading) { 9372 tinymce.each(queueLoadedCallbacks, function(callback) { 9373 callback.func.call(callback.scope); 9374 }); 9375 9376 queueLoadedCallbacks.length = 0; 9377 } 9378 }; 9379 9380 loadScripts(); 9381 }; 9382 }; 9383 9384 // Global script loader 9385 tinymce.ScriptLoader = new tinymce.dom.ScriptLoader(); 9386 })(tinymce); 9387 9388 (function(tinymce) { 9389 tinymce.dom.RangeUtils = function(dom) { 9390 var INVISIBLE_CHAR = '\uFEFF'; 9391 9392 this.walk = function(rng, callback) { 9393 var startContainer = rng.startContainer, 9394 startOffset = rng.startOffset, 9395 endContainer = rng.endContainer, 9396 endOffset = rng.endOffset, 9397 ancestor, startPoint, 9398 endPoint, node, parent, siblings, nodes; 9399 9400 // Handle table cell selection the table plugin enables 9401 // you to fake select table cells and perform formatting actions on them 9402 nodes = dom.select('td.mceSelected,th.mceSelected'); 9403 if (nodes.length > 0) { 9404 tinymce.each(nodes, function(node) { 9405 callback([node]); 9406 }); 9407 9408 return; 9409 } 9410 9411 function exclude(nodes) { 9412 var node; 9413 9414 // First node is excluded 9415 node = nodes[0]; 9416 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 9417 nodes.splice(0, 1); 9418 } 9419 9420 // Last node is excluded 9421 node = nodes[nodes.length - 1]; 9422 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 9423 nodes.splice(nodes.length - 1, 1); 9424 } 9425 9426 return nodes; 9427 }; 9428 9429 function collectSiblings(node, name, end_node) { 9430 var siblings = []; 9431 9432 for (; node && node != end_node; node = node[name]) 9433 siblings.push(node); 9434 9435 return siblings; 9436 }; 9437 9438 function findEndPoint(node, root) { 9439 do { 9440 if (node.parentNode == root) 9441 return node; 9442 9443 node = node.parentNode; 9444 } while(node); 9445 }; 9446 9447 function walkBoundary(start_node, end_node, next) { 9448 var siblingName = next ? 'nextSibling' : 'previousSibling'; 9449 9450 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 9451 parent = node.parentNode; 9452 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 9453 9454 if (siblings.length) { 9455 if (!next) 9456 siblings.reverse(); 9457 9458 callback(exclude(siblings)); 9459 } 9460 } 9461 }; 9462 9463 // If index based start position then resolve it 9464 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) 9465 startContainer = startContainer.childNodes[startOffset]; 9466 9467 // If index based end position then resolve it 9468 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) 9469 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; 9470 9471 // Same container 9472 if (startContainer == endContainer) 9473 return callback(exclude([startContainer])); 9474 9475 // Find common ancestor and end points 9476 ancestor = dom.findCommonAncestor(startContainer, endContainer); 9477 9478 // Process left side 9479 for (node = startContainer; node; node = node.parentNode) { 9480 if (node === endContainer) 9481 return walkBoundary(startContainer, ancestor, true); 9482 9483 if (node === ancestor) 9484 break; 9485 } 9486 9487 // Process right side 9488 for (node = endContainer; node; node = node.parentNode) { 9489 if (node === startContainer) 9490 return walkBoundary(endContainer, ancestor); 9491 9492 if (node === ancestor) 9493 break; 9494 } 9495 9496 // Find start/end point 9497 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 9498 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 9499 9500 // Walk left leaf 9501 walkBoundary(startContainer, startPoint, true); 9502 9503 // Walk the middle from start to end point 9504 siblings = collectSiblings( 9505 startPoint == startContainer ? startPoint : startPoint.nextSibling, 9506 'nextSibling', 9507 endPoint == endContainer ? endPoint.nextSibling : endPoint 9508 ); 9509 9510 if (siblings.length) 9511 callback(exclude(siblings)); 9512 9513 // Walk right leaf 9514 walkBoundary(endContainer, endPoint); 9515 }; 9516 9517 this.split = function(rng) { 9518 var startContainer = rng.startContainer, 9519 startOffset = rng.startOffset, 9520 endContainer = rng.endContainer, 9521 endOffset = rng.endOffset; 9522 9523 function splitText(node, offset) { 9524 return node.splitText(offset); 9525 }; 9526 9527 // Handle single text node 9528 if (startContainer == endContainer && startContainer.nodeType == 3) { 9529 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9530 endContainer = splitText(startContainer, startOffset); 9531 startContainer = endContainer.previousSibling; 9532 9533 if (endOffset > startOffset) { 9534 endOffset = endOffset - startOffset; 9535 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 9536 endOffset = endContainer.nodeValue.length; 9537 startOffset = 0; 9538 } else { 9539 endOffset = 0; 9540 } 9541 } 9542 } else { 9543 // Split startContainer text node if needed 9544 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 9545 startContainer = splitText(startContainer, startOffset); 9546 startOffset = 0; 9547 } 9548 9549 // Split endContainer text node if needed 9550 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 9551 endContainer = splitText(endContainer, endOffset).previousSibling; 9552 endOffset = endContainer.nodeValue.length; 9553 } 9554 } 9555 9556 return { 9557 startContainer : startContainer, 9558 startOffset : startOffset, 9559 endContainer : endContainer, 9560 endOffset : endOffset 9561 }; 9562 }; 9563 9564 }; 9565 9566 tinymce.dom.RangeUtils.compareRanges = function(rng1, rng2) { 9567 if (rng1 && rng2) { 9568 // Compare native IE ranges 9569 if (rng1.item || rng1.duplicate) { 9570 // Both are control ranges and the selected element matches 9571 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) 9572 return true; 9573 9574 // Both are text ranges and the range matches 9575 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) 9576 return true; 9577 } else { 9578 // Compare w3c ranges 9579 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 9580 } 9581 } 9582 9583 return false; 9584 }; 9585 })(tinymce); 9586 9587 (function(tinymce) { 9588 var Event = tinymce.dom.Event, each = tinymce.each; 9589 9590 tinymce.create('tinymce.ui.KeyboardNavigation', { 9591 KeyboardNavigation: function(settings, dom) { 9592 var t = this, root = settings.root, items = settings.items, 9593 enableUpDown = settings.enableUpDown, enableLeftRight = settings.enableLeftRight || !settings.enableUpDown, 9594 excludeFromTabOrder = settings.excludeFromTabOrder, 9595 itemFocussed, itemBlurred, rootKeydown, rootFocussed, focussedId; 9596 9597 dom = dom || tinymce.DOM; 9598 9599 itemFocussed = function(evt) { 9600 focussedId = evt.target.id; 9601 }; 9602 9603 itemBlurred = function(evt) { 9604 dom.setAttrib(evt.target.id, 'tabindex', '-1'); 9605 }; 9606 9607 rootFocussed = function(evt) { 9608 var item = dom.get(focussedId); 9609 dom.setAttrib(item, 'tabindex', '0'); 9610 item.focus(); 9611 }; 9612 9613 t.focus = function() { 9614 dom.get(focussedId).focus(); 9615 }; 9616 9617 t.destroy = function() { 9618 each(items, function(item) { 9619 var elm = dom.get(item.id); 9620 9621 dom.unbind(elm, 'focus', itemFocussed); 9622 dom.unbind(elm, 'blur', itemBlurred); 9623 }); 9624 9625 var rootElm = dom.get(root); 9626 dom.unbind(rootElm, 'focus', rootFocussed); 9627 dom.unbind(rootElm, 'keydown', rootKeydown); 9628 9629 items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; 9630 t.destroy = function() {}; 9631 }; 9632 9633 t.moveFocus = function(dir, evt) { 9634 var idx = -1, controls = t.controls, newFocus; 9635 9636 if (!focussedId) 9637 return; 9638 9639 each(items, function(item, index) { 9640 if (item.id === focussedId) { 9641 idx = index; 9642 return false; 9643 } 9644 }); 9645 9646 idx += dir; 9647 if (idx < 0) { 9648 idx = items.length - 1; 9649 } else if (idx >= items.length) { 9650 idx = 0; 9651 } 9652 9653 newFocus = items[idx]; 9654 dom.setAttrib(focussedId, 'tabindex', '-1'); 9655 dom.setAttrib(newFocus.id, 'tabindex', '0'); 9656 dom.get(newFocus.id).focus(); 9657 9658 if (settings.actOnFocus) { 9659 settings.onAction(newFocus.id); 9660 } 9661 9662 if (evt) 9663 Event.cancel(evt); 9664 }; 9665 9666 rootKeydown = function(evt) { 9667 var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40, DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32; 9668 9669 switch (evt.keyCode) { 9670 case DOM_VK_LEFT: 9671 if (enableLeftRight) t.moveFocus(-1); 9672 break; 9673 9674 case DOM_VK_RIGHT: 9675 if (enableLeftRight) t.moveFocus(1); 9676 break; 9677 9678 case DOM_VK_UP: 9679 if (enableUpDown) t.moveFocus(-1); 9680 break; 9681 9682 case DOM_VK_DOWN: 9683 if (enableUpDown) t.moveFocus(1); 9684 break; 9685 9686 case DOM_VK_ESCAPE: 9687 if (settings.onCancel) { 9688 settings.onCancel(); 9689 Event.cancel(evt); 9690 } 9691 break; 9692 9693 case DOM_VK_ENTER: 9694 case DOM_VK_RETURN: 9695 case DOM_VK_SPACE: 9696 if (settings.onAction) { 9697 settings.onAction(focussedId); 9698 Event.cancel(evt); 9699 } 9700 break; 9701 } 9702 }; 9703 9704 // Set up state and listeners for each item. 9705 each(items, function(item, idx) { 9706 var tabindex, elm; 9707 9708 if (!item.id) { 9709 item.id = dom.uniqueId('_mce_item_'); 9710 } 9711 9712 elm = dom.get(item.id); 9713 9714 if (excludeFromTabOrder) { 9715 dom.bind(elm, 'blur', itemBlurred); 9716 tabindex = '-1'; 9717 } else { 9718 tabindex = (idx === 0 ? '0' : '-1'); 9719 } 9720 9721 elm.setAttribute('tabindex', tabindex); 9722 dom.bind(elm, 'focus', itemFocussed); 9723 }); 9724 9725 // Setup initial state for root element. 9726 if (items[0]){ 9727 focussedId = items[0].id; 9728 } 9729 9730 dom.setAttrib(root, 'tabindex', '-1'); 9731 9732 // Setup listeners for root element. 9733 var rootElm = dom.get(root); 9734 dom.bind(rootElm, 'focus', rootFocussed); 9735 dom.bind(rootElm, 'keydown', rootKeydown); 9736 } 9737 }); 9738 })(tinymce); 9739 9740 (function(tinymce) { 9741 // Shorten class names 9742 var DOM = tinymce.DOM, is = tinymce.is; 9743 9744 tinymce.create('tinymce.ui.Control', { 9745 Control : function(id, s, editor) { 9746 this.id = id; 9747 this.settings = s = s || {}; 9748 this.rendered = false; 9749 this.onRender = new tinymce.util.Dispatcher(this); 9750 this.classPrefix = ''; 9751 this.scope = s.scope || this; 9752 this.disabled = 0; 9753 this.active = 0; 9754 this.editor = editor; 9755 }, 9756 9757 setAriaProperty : function(property, value) { 9758 var element = DOM.get(this.id + '_aria') || DOM.get(this.id); 9759 if (element) { 9760 DOM.setAttrib(element, 'aria-' + property, !!value); 9761 } 9762 }, 9763 9764 focus : function() { 9765 DOM.get(this.id).focus(); 9766 }, 9767 9768 setDisabled : function(s) { 9769 if (s != this.disabled) { 9770 this.setAriaProperty('disabled', s); 9771 9772 this.setState('Disabled', s); 9773 this.setState('Enabled', !s); 9774 this.disabled = s; 9775 } 9776 }, 9777 9778 isDisabled : function() { 9779 return this.disabled; 9780 }, 9781 9782 setActive : function(s) { 9783 if (s != this.active) { 9784 this.setState('Active', s); 9785 this.active = s; 9786 this.setAriaProperty('pressed', s); 9787 } 9788 }, 9789 9790 isActive : function() { 9791 return this.active; 9792 }, 9793 9794 setState : function(c, s) { 9795 var n = DOM.get(this.id); 9796 9797 c = this.classPrefix + c; 9798 9799 if (s) 9800 DOM.addClass(n, c); 9801 else 9802 DOM.removeClass(n, c); 9803 }, 9804 9805 isRendered : function() { 9806 return this.rendered; 9807 }, 9808 9809 renderHTML : function() { 9810 }, 9811 9812 renderTo : function(n) { 9813 DOM.setHTML(n, this.renderHTML()); 9814 }, 9815 9816 postRender : function() { 9817 var t = this, b; 9818 9819 // Set pending states 9820 if (is(t.disabled)) { 9821 b = t.disabled; 9822 t.disabled = -1; 9823 t.setDisabled(b); 9824 } 9825 9826 if (is(t.active)) { 9827 b = t.active; 9828 t.active = -1; 9829 t.setActive(b); 9830 } 9831 }, 9832 9833 remove : function() { 9834 DOM.remove(this.id); 9835 this.destroy(); 9836 }, 9837 9838 destroy : function() { 9839 tinymce.dom.Event.clear(this.id); 9840 } 9841 }); 9842 })(tinymce); 9843 tinymce.create('tinymce.ui.Container:tinymce.ui.Control', { 9844 Container : function(id, s, editor) { 9845 this.parent(id, s, editor); 9846 9847 this.controls = []; 9848 9849 this.lookup = {}; 9850 }, 9851 9852 add : function(c) { 9853 this.lookup[c.id] = c; 9854 this.controls.push(c); 9855 9856 return c; 9857 }, 9858 9859 get : function(n) { 9860 return this.lookup[n]; 9861 } 9862 }); 9863 9864 9865 tinymce.create('tinymce.ui.Separator:tinymce.ui.Control', { 9866 Separator : function(id, s) { 9867 this.parent(id, s); 9868 this.classPrefix = 'mceSeparator'; 9869 this.setDisabled(true); 9870 }, 9871 9872 renderHTML : function() { 9873 return tinymce.DOM.createHTML('span', {'class' : this.classPrefix, role : 'separator', 'aria-orientation' : 'vertical', tabindex : '-1'}); 9874 } 9875 }); 9876 9877 (function(tinymce) { 9878 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9879 9880 tinymce.create('tinymce.ui.MenuItem:tinymce.ui.Control', { 9881 MenuItem : function(id, s) { 9882 this.parent(id, s); 9883 this.classPrefix = 'mceMenuItem'; 9884 }, 9885 9886 setSelected : function(s) { 9887 this.setState('Selected', s); 9888 this.setAriaProperty('checked', !!s); 9889 this.selected = s; 9890 }, 9891 9892 isSelected : function() { 9893 return this.selected; 9894 }, 9895 9896 postRender : function() { 9897 var t = this; 9898 9899 t.parent(); 9900 9901 // Set pending state 9902 if (is(t.selected)) 9903 t.setSelected(t.selected); 9904 } 9905 }); 9906 })(tinymce); 9907 9908 (function(tinymce) { 9909 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, walk = tinymce.walk; 9910 9911 tinymce.create('tinymce.ui.Menu:tinymce.ui.MenuItem', { 9912 Menu : function(id, s) { 9913 var t = this; 9914 9915 t.parent(id, s); 9916 t.items = {}; 9917 t.collapsed = false; 9918 t.menuCount = 0; 9919 t.onAddItem = new tinymce.util.Dispatcher(this); 9920 }, 9921 9922 expand : function(d) { 9923 var t = this; 9924 9925 if (d) { 9926 walk(t, function(o) { 9927 if (o.expand) 9928 o.expand(); 9929 }, 'items', t); 9930 } 9931 9932 t.collapsed = false; 9933 }, 9934 9935 collapse : function(d) { 9936 var t = this; 9937 9938 if (d) { 9939 walk(t, function(o) { 9940 if (o.collapse) 9941 o.collapse(); 9942 }, 'items', t); 9943 } 9944 9945 t.collapsed = true; 9946 }, 9947 9948 isCollapsed : function() { 9949 return this.collapsed; 9950 }, 9951 9952 add : function(o) { 9953 if (!o.settings) 9954 o = new tinymce.ui.MenuItem(o.id || DOM.uniqueId(), o); 9955 9956 this.onAddItem.dispatch(this, o); 9957 9958 return this.items[o.id] = o; 9959 }, 9960 9961 addSeparator : function() { 9962 return this.add({separator : true}); 9963 }, 9964 9965 addMenu : function(o) { 9966 if (!o.collapse) 9967 o = this.createMenu(o); 9968 9969 this.menuCount++; 9970 9971 return this.add(o); 9972 }, 9973 9974 hasMenus : function() { 9975 return this.menuCount !== 0; 9976 }, 9977 9978 remove : function(o) { 9979 delete this.items[o.id]; 9980 }, 9981 9982 removeAll : function() { 9983 var t = this; 9984 9985 walk(t, function(o) { 9986 if (o.removeAll) 9987 o.removeAll(); 9988 else 9989 o.remove(); 9990 9991 o.destroy(); 9992 }, 'items', t); 9993 9994 t.items = {}; 9995 }, 9996 9997 createMenu : function(o) { 9998 var m = new tinymce.ui.Menu(o.id || DOM.uniqueId(), o); 9999 10000 m.onAddItem.add(this.onAddItem.dispatch, this.onAddItem); 10001 10002 return m; 10003 } 10004 }); 10005 })(tinymce); 10006 (function(tinymce) { 10007 var is = tinymce.is, DOM = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event, Element = tinymce.dom.Element; 10008 10009 tinymce.create('tinymce.ui.DropMenu:tinymce.ui.Menu', { 10010 DropMenu : function(id, s) { 10011 s = s || {}; 10012 s.container = s.container || DOM.doc.body; 10013 s.offset_x = s.offset_x || 0; 10014 s.offset_y = s.offset_y || 0; 10015 s.vp_offset_x = s.vp_offset_x || 0; 10016 s.vp_offset_y = s.vp_offset_y || 0; 10017 10018 if (is(s.icons) && !s.icons) 10019 s['class'] += ' mceNoIcons'; 10020 10021 this.parent(id, s); 10022 this.onShowMenu = new tinymce.util.Dispatcher(this); 10023 this.onHideMenu = new tinymce.util.Dispatcher(this); 10024 this.classPrefix = 'mceMenu'; 10025 }, 10026 10027 createMenu : function(s) { 10028 var t = this, cs = t.settings, m; 10029 10030 s.container = s.container || cs.container; 10031 s.parent = t; 10032 s.constrain = s.constrain || cs.constrain; 10033 s['class'] = s['class'] || cs['class']; 10034 s.vp_offset_x = s.vp_offset_x || cs.vp_offset_x; 10035 s.vp_offset_y = s.vp_offset_y || cs.vp_offset_y; 10036 s.keyboard_focus = cs.keyboard_focus; 10037 m = new tinymce.ui.DropMenu(s.id || DOM.uniqueId(), s); 10038 10039 m.onAddItem.add(t.onAddItem.dispatch, t.onAddItem); 10040 10041 return m; 10042 }, 10043 10044 focus : function() { 10045 var t = this; 10046 if (t.keyboardNav) { 10047 t.keyboardNav.focus(); 10048 } 10049 }, 10050 10051 update : function() { 10052 var t = this, s = t.settings, tb = DOM.get('menu_' + t.id + '_tbl'), co = DOM.get('menu_' + t.id + '_co'), tw, th; 10053 10054 tw = s.max_width ? Math.min(tb.offsetWidth, s.max_width) : tb.offsetWidth; 10055 th = s.max_height ? Math.min(tb.offsetHeight, s.max_height) : tb.offsetHeight; 10056 10057 if (!DOM.boxModel) 10058 t.element.setStyles({width : tw + 2, height : th + 2}); 10059 else 10060 t.element.setStyles({width : tw, height : th}); 10061 10062 if (s.max_width) 10063 DOM.setStyle(co, 'width', tw); 10064 10065 if (s.max_height) { 10066 DOM.setStyle(co, 'height', th); 10067 10068 if (tb.clientHeight < s.max_height) 10069 DOM.setStyle(co, 'overflow', 'hidden'); 10070 } 10071 }, 10072 10073 showMenu : function(x, y, px) { 10074 var t = this, s = t.settings, co, vp = DOM.getViewPort(), w, h, mx, my, ot = 2, dm, tb, cp = t.classPrefix; 10075 10076 t.collapse(1); 10077 10078 if (t.isMenuVisible) 10079 return; 10080 10081 if (!t.rendered) { 10082 co = DOM.add(t.settings.container, t.renderNode()); 10083 10084 each(t.items, function(o) { 10085 o.postRender(); 10086 }); 10087 10088 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10089 } else 10090 co = DOM.get('menu_' + t.id); 10091 10092 // Move layer out of sight unless it's Opera since it scrolls to top of page due to an bug 10093 if (!tinymce.isOpera) 10094 DOM.setStyles(co, {left : -0xFFFF , top : -0xFFFF}); 10095 10096 DOM.show(co); 10097 t.update(); 10098 10099 x += s.offset_x || 0; 10100 y += s.offset_y || 0; 10101 vp.w -= 4; 10102 vp.h -= 4; 10103 10104 // Move inside viewport if not submenu 10105 if (s.constrain) { 10106 w = co.clientWidth - ot; 10107 h = co.clientHeight - ot; 10108 mx = vp.x + vp.w; 10109 my = vp.y + vp.h; 10110 10111 if ((x + s.vp_offset_x + w) > mx) 10112 x = px ? px - w : Math.max(0, (mx - s.vp_offset_x) - w); 10113 10114 if ((y + s.vp_offset_y + h) > my) 10115 y = Math.max(0, (my - s.vp_offset_y) - h); 10116 } 10117 10118 DOM.setStyles(co, {left : x , top : y}); 10119 t.element.update(); 10120 10121 t.isMenuVisible = 1; 10122 t.mouseClickFunc = Event.add(co, 'click', function(e) { 10123 var m; 10124 10125 e = e.target; 10126 10127 if (e && (e = DOM.getParent(e, 'tr')) && !DOM.hasClass(e, cp + 'ItemSub')) { 10128 m = t.items[e.id]; 10129 10130 if (m.isDisabled()) 10131 return; 10132 10133 dm = t; 10134 10135 while (dm) { 10136 if (dm.hideMenu) 10137 dm.hideMenu(); 10138 10139 dm = dm.settings.parent; 10140 } 10141 10142 if (m.settings.onclick) 10143 m.settings.onclick(e); 10144 10145 return false; // Cancel to fix onbeforeunload problem 10146 } 10147 }); 10148 10149 if (t.hasMenus()) { 10150 t.mouseOverFunc = Event.add(co, 'mouseover', function(e) { 10151 var m, r, mi; 10152 10153 e = e.target; 10154 if (e && (e = DOM.getParent(e, 'tr'))) { 10155 m = t.items[e.id]; 10156 10157 if (t.lastMenu) 10158 t.lastMenu.collapse(1); 10159 10160 if (m.isDisabled()) 10161 return; 10162 10163 if (e && DOM.hasClass(e, cp + 'ItemSub')) { 10164 //p = DOM.getPos(s.container); 10165 r = DOM.getRect(e); 10166 m.showMenu((r.x + r.w - ot), r.y - ot, r.x); 10167 t.lastMenu = m; 10168 DOM.addClass(DOM.get(m.id).firstChild, cp + 'ItemActive'); 10169 } 10170 } 10171 }); 10172 } 10173 10174 Event.add(co, 'keydown', t._keyHandler, t); 10175 10176 t.onShowMenu.dispatch(t); 10177 10178 if (s.keyboard_focus) { 10179 t._setupKeyboardNav(); 10180 } 10181 }, 10182 10183 hideMenu : function(c) { 10184 var t = this, co = DOM.get('menu_' + t.id), e; 10185 10186 if (!t.isMenuVisible) 10187 return; 10188 10189 if (t.keyboardNav) t.keyboardNav.destroy(); 10190 Event.remove(co, 'mouseover', t.mouseOverFunc); 10191 Event.remove(co, 'click', t.mouseClickFunc); 10192 Event.remove(co, 'keydown', t._keyHandler); 10193 DOM.hide(co); 10194 t.isMenuVisible = 0; 10195 10196 if (!c) 10197 t.collapse(1); 10198 10199 if (t.element) 10200 t.element.hide(); 10201 10202 if (e = DOM.get(t.id)) 10203 DOM.removeClass(e.firstChild, t.classPrefix + 'ItemActive'); 10204 10205 t.onHideMenu.dispatch(t); 10206 }, 10207 10208 add : function(o) { 10209 var t = this, co; 10210 10211 o = t.parent(o); 10212 10213 if (t.isRendered && (co = DOM.get('menu_' + t.id))) 10214 t._add(DOM.select('tbody', co)[0], o); 10215 10216 return o; 10217 }, 10218 10219 collapse : function(d) { 10220 this.parent(d); 10221 this.hideMenu(1); 10222 }, 10223 10224 remove : function(o) { 10225 DOM.remove(o.id); 10226 this.destroy(); 10227 10228 return this.parent(o); 10229 }, 10230 10231 destroy : function() { 10232 var t = this, co = DOM.get('menu_' + t.id); 10233 10234 if (t.keyboardNav) t.keyboardNav.destroy(); 10235 Event.remove(co, 'mouseover', t.mouseOverFunc); 10236 Event.remove(DOM.select('a', co), 'focus', t.mouseOverFunc); 10237 Event.remove(co, 'click', t.mouseClickFunc); 10238 Event.remove(co, 'keydown', t._keyHandler); 10239 10240 if (t.element) 10241 t.element.remove(); 10242 10243 DOM.remove(co); 10244 }, 10245 10246 renderNode : function() { 10247 var t = this, s = t.settings, n, tb, co, w; 10248 10249 w = DOM.create('div', {role: 'listbox', id : 'menu_' + t.id, 'class' : s['class'], 'style' : 'position:absolute;left:0;top:0;z-index:200000;outline:0'}); 10250 if (t.settings.parent) { 10251 DOM.setAttrib(w, 'aria-parent', 'menu_' + t.settings.parent.id); 10252 } 10253 co = DOM.add(w, 'div', {role: 'presentation', id : 'menu_' + t.id + '_co', 'class' : t.classPrefix + (s['class'] ? ' ' + s['class'] : '')}); 10254 t.element = new Element('menu_' + t.id, {blocker : 1, container : s.container}); 10255 10256 if (s.menu_line) 10257 DOM.add(co, 'span', {'class' : t.classPrefix + 'Line'}); 10258 10259 // n = DOM.add(co, 'div', {id : 'menu_' + t.id + '_co', 'class' : 'mceMenuContainer'}); 10260 n = DOM.add(co, 'table', {role: 'presentation', id : 'menu_' + t.id + '_tbl', border : 0, cellPadding : 0, cellSpacing : 0}); 10261 tb = DOM.add(n, 'tbody'); 10262 10263 each(t.items, function(o) { 10264 t._add(tb, o); 10265 }); 10266 10267 t.rendered = true; 10268 10269 return w; 10270 }, 10271 10272 // Internal functions 10273 _setupKeyboardNav : function(){ 10274 var contextMenu, menuItems, t=this; 10275 contextMenu = DOM.get('menu_' + t.id); 10276 menuItems = DOM.select('a[role=option]', 'menu_' + t.id); 10277 menuItems.splice(0,0,contextMenu); 10278 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 10279 root: 'menu_' + t.id, 10280 items: menuItems, 10281 onCancel: function() { 10282 t.hideMenu(); 10283 }, 10284 enableUpDown: true 10285 }); 10286 contextMenu.focus(); 10287 }, 10288 10289 _keyHandler : function(evt) { 10290 var t = this, e; 10291 switch (evt.keyCode) { 10292 case 37: // Left 10293 if (t.settings.parent) { 10294 t.hideMenu(); 10295 t.settings.parent.focus(); 10296 Event.cancel(evt); 10297 } 10298 break; 10299 case 39: // Right 10300 if (t.mouseOverFunc) 10301 t.mouseOverFunc(evt); 10302 break; 10303 } 10304 }, 10305 10306 _add : function(tb, o) { 10307 var n, s = o.settings, a, ro, it, cp = this.classPrefix, ic; 10308 10309 if (s.separator) { 10310 ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'ItemSeparator'}); 10311 DOM.add(ro, 'td', {'class' : cp + 'ItemSeparator'}); 10312 10313 if (n = ro.previousSibling) 10314 DOM.addClass(n, 'mceLast'); 10315 10316 return; 10317 } 10318 10319 n = ro = DOM.add(tb, 'tr', {id : o.id, 'class' : cp + 'Item ' + cp + 'ItemEnabled'}); 10320 n = it = DOM.add(n, s.titleItem ? 'th' : 'td'); 10321 n = a = DOM.add(n, 'a', {id: o.id + '_aria', role: s.titleItem ? 'presentation' : 'option', href : 'javascript:;', onclick : "return false;", onmousedown : 'return false;'}); 10322 10323 if (s.parent) { 10324 DOM.setAttrib(a, 'aria-haspopup', 'true'); 10325 DOM.setAttrib(a, 'aria-owns', 'menu_' + o.id); 10326 } 10327 10328 DOM.addClass(it, s['class']); 10329 // n = DOM.add(n, 'span', {'class' : 'item'}); 10330 10331 ic = DOM.add(n, 'span', {'class' : 'mceIcon' + (s.icon ? ' mce_' + s.icon : '')}); 10332 10333 if (s.icon_src) 10334 DOM.add(ic, 'img', {src : s.icon_src}); 10335 10336 n = DOM.add(n, s.element || 'span', {'class' : 'mceText', title : o.settings.title}, o.settings.title); 10337 10338 if (o.settings.style) { 10339 if (typeof o.settings.style == "function") 10340 o.settings.style = o.settings.style(); 10341 10342 DOM.setAttrib(n, 'style', o.settings.style); 10343 } 10344 10345 if (tb.childNodes.length == 1) 10346 DOM.addClass(ro, 'mceFirst'); 10347 10348 if ((n = ro.previousSibling) && DOM.hasClass(n, cp + 'ItemSeparator')) 10349 DOM.addClass(ro, 'mceFirst'); 10350 10351 if (o.collapse) 10352 DOM.addClass(ro, cp + 'ItemSub'); 10353 10354 if (n = ro.previousSibling) 10355 DOM.removeClass(n, 'mceLast'); 10356 10357 DOM.addClass(ro, 'mceLast'); 10358 } 10359 }); 10360 })(tinymce); 10361 (function(tinymce) { 10362 var DOM = tinymce.DOM; 10363 10364 tinymce.create('tinymce.ui.Button:tinymce.ui.Control', { 10365 Button : function(id, s, ed) { 10366 this.parent(id, s, ed); 10367 this.classPrefix = 'mceButton'; 10368 }, 10369 10370 renderHTML : function() { 10371 var cp = this.classPrefix, s = this.settings, h, l; 10372 10373 l = DOM.encode(s.label || ''); 10374 h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">'; 10375 if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) 10376 h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10377 else 10378 h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); 10379 10380 h += '<span class="mceVoiceLabel mceIconOnly" style="display: none;" id="' + this.id + '_voice">' + s.title + '</span>'; 10381 h += '</a>'; 10382 return h; 10383 }, 10384 10385 postRender : function() { 10386 var t = this, s = t.settings, imgBookmark; 10387 10388 // In IE a large image that occupies the entire editor area will be deselected when a button is clicked, so 10389 // need to keep the selection in case the selection is lost 10390 if (tinymce.isIE && t.editor) { 10391 tinymce.dom.Event.add(t.id, 'mousedown', function(e) { 10392 var nodeName = t.editor.selection.getNode().nodeName; 10393 imgBookmark = nodeName === 'IMG' ? t.editor.selection.getBookmark() : null; 10394 }); 10395 } 10396 tinymce.dom.Event.add(t.id, 'click', function(e) { 10397 if (!t.isDisabled()) { 10398 // restore the selection in case the selection is lost in IE 10399 if (tinymce.isIE && t.editor && imgBookmark !== null) { 10400 t.editor.selection.moveToBookmark(imgBookmark); 10401 } 10402 return s.onclick.call(s.scope, e); 10403 } 10404 }); 10405 tinymce.dom.Event.add(t.id, 'keyup', function(e) { 10406 if (!t.isDisabled() && e.keyCode==tinymce.VK.SPACEBAR) 10407 return s.onclick.call(s.scope, e); 10408 }); 10409 } 10410 }); 10411 })(tinymce); 10412 10413 (function(tinymce) { 10414 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10415 10416 tinymce.create('tinymce.ui.ListBox:tinymce.ui.Control', { 10417 ListBox : function(id, s, ed) { 10418 var t = this; 10419 10420 t.parent(id, s, ed); 10421 10422 t.items = []; 10423 10424 t.onChange = new Dispatcher(t); 10425 10426 t.onPostRender = new Dispatcher(t); 10427 10428 t.onAdd = new Dispatcher(t); 10429 10430 t.onRenderMenu = new tinymce.util.Dispatcher(this); 10431 10432 t.classPrefix = 'mceListBox'; 10433 t.marked = {}; 10434 }, 10435 10436 select : function(va) { 10437 var t = this, fv, f; 10438 10439 t.marked = {}; 10440 10441 if (va == undef) 10442 return t.selectByIndex(-1); 10443 10444 // Is string or number make function selector 10445 if (va && typeof(va)=="function") 10446 f = va; 10447 else { 10448 f = function(v) { 10449 return v == va; 10450 }; 10451 } 10452 10453 // Do we need to do something? 10454 if (va != t.selectedValue) { 10455 // Find item 10456 each(t.items, function(o, i) { 10457 if (f(o.value)) { 10458 fv = 1; 10459 t.selectByIndex(i); 10460 return false; 10461 } 10462 }); 10463 10464 if (!fv) 10465 t.selectByIndex(-1); 10466 } 10467 }, 10468 10469 selectByIndex : function(idx) { 10470 var t = this, e, o, label; 10471 10472 t.marked = {}; 10473 10474 if (idx != t.selectedIndex) { 10475 e = DOM.get(t.id + '_text'); 10476 label = DOM.get(t.id + '_voiceDesc'); 10477 o = t.items[idx]; 10478 10479 if (o) { 10480 t.selectedValue = o.value; 10481 t.selectedIndex = idx; 10482 DOM.setHTML(e, DOM.encode(o.title)); 10483 DOM.setHTML(label, t.settings.title + " - " + o.title); 10484 DOM.removeClass(e, 'mceTitle'); 10485 DOM.setAttrib(t.id, 'aria-valuenow', o.title); 10486 } else { 10487 DOM.setHTML(e, DOM.encode(t.settings.title)); 10488 DOM.setHTML(label, DOM.encode(t.settings.title)); 10489 DOM.addClass(e, 'mceTitle'); 10490 t.selectedValue = t.selectedIndex = null; 10491 DOM.setAttrib(t.id, 'aria-valuenow', t.settings.title); 10492 } 10493 e = 0; 10494 } 10495 }, 10496 10497 mark : function(value) { 10498 this.marked[value] = true; 10499 }, 10500 10501 add : function(n, v, o) { 10502 var t = this; 10503 10504 o = o || {}; 10505 o = tinymce.extend(o, { 10506 title : n, 10507 value : v 10508 }); 10509 10510 t.items.push(o); 10511 t.onAdd.dispatch(t, o); 10512 }, 10513 10514 getLength : function() { 10515 return this.items.length; 10516 }, 10517 10518 renderHTML : function() { 10519 var h = '', t = this, s = t.settings, cp = t.classPrefix; 10520 10521 h = '<span role="listbox" aria-haspopup="true" aria-labelledby="' + t.id +'_voiceDesc" aria-describedby="' + t.id + '_voiceDesc"><table role="presentation" tabindex="0" id="' + t.id + '" cellpadding="0" cellspacing="0" class="' + cp + ' ' + cp + 'Enabled' + (s['class'] ? (' ' + s['class']) : '') + '"><tbody><tr>'; 10522 h += '<td>' + DOM.createHTML('span', {id: t.id + '_voiceDesc', 'class': 'voiceLabel', style:'display:none;'}, t.settings.title); 10523 h += DOM.createHTML('a', {id : t.id + '_text', tabindex : -1, href : 'javascript:;', 'class' : 'mceText', onclick : "return false;", onmousedown : 'return false;'}, DOM.encode(t.settings.title)) + '</td>'; 10524 h += '<td>' + DOM.createHTML('a', {id : t.id + '_open', tabindex : -1, href : 'javascript:;', 'class' : 'mceOpen', onclick : "return false;", onmousedown : 'return false;'}, '<span><span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span></span>') + '</td>'; 10525 h += '</tr></tbody></table></span>'; 10526 10527 return h; 10528 }, 10529 10530 showMenu : function() { 10531 var t = this, p2, e = DOM.get(this.id), m; 10532 10533 if (t.isDisabled() || t.items.length === 0) 10534 return; 10535 10536 if (t.menu && t.menu.isMenuVisible) 10537 return t.hideMenu(); 10538 10539 if (!t.isMenuRendered) { 10540 t.renderMenu(); 10541 t.isMenuRendered = true; 10542 } 10543 10544 p2 = DOM.getPos(e); 10545 10546 m = t.menu; 10547 m.settings.offset_x = p2.x; 10548 m.settings.offset_y = p2.y; 10549 m.settings.keyboard_focus = !tinymce.isOpera; // Opera is buggy when it comes to auto focus 10550 10551 // Select in menu 10552 each(t.items, function(o) { 10553 if (m.items[o.id]) { 10554 m.items[o.id].setSelected(0); 10555 } 10556 }); 10557 10558 each(t.items, function(o) { 10559 if (m.items[o.id] && t.marked[o.value]) { 10560 m.items[o.id].setSelected(1); 10561 } 10562 10563 if (o.value === t.selectedValue) { 10564 m.items[o.id].setSelected(1); 10565 } 10566 }); 10567 10568 m.showMenu(0, e.clientHeight); 10569 10570 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10571 DOM.addClass(t.id, t.classPrefix + 'Selected'); 10572 10573 //DOM.get(t.id + '_text').focus(); 10574 }, 10575 10576 hideMenu : function(e) { 10577 var t = this; 10578 10579 if (t.menu && t.menu.isMenuVisible) { 10580 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10581 10582 // Prevent double toogles by canceling the mouse click event to the button 10583 if (e && e.type == "mousedown" && (e.target.id == t.id + '_text' || e.target.id == t.id + '_open')) 10584 return; 10585 10586 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10587 DOM.removeClass(t.id, t.classPrefix + 'Selected'); 10588 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10589 t.menu.hideMenu(); 10590 } 10591 } 10592 }, 10593 10594 renderMenu : function() { 10595 var t = this, m; 10596 10597 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10598 menu_line : 1, 10599 'class' : t.classPrefix + 'Menu mceNoIcons', 10600 max_width : 250, 10601 max_height : 150 10602 }); 10603 10604 m.onHideMenu.add(function() { 10605 t.hideMenu(); 10606 t.focus(); 10607 }); 10608 10609 m.add({ 10610 title : t.settings.title, 10611 'class' : 'mceMenuItemTitle', 10612 onclick : function() { 10613 if (t.settings.onselect('') !== false) 10614 t.select(''); // Must be runned after 10615 } 10616 }); 10617 10618 each(t.items, function(o) { 10619 // No value then treat it as a title 10620 if (o.value === undef) { 10621 m.add({ 10622 title : o.title, 10623 role : "option", 10624 'class' : 'mceMenuItemTitle', 10625 onclick : function() { 10626 if (t.settings.onselect('') !== false) 10627 t.select(''); // Must be runned after 10628 } 10629 }); 10630 } else { 10631 o.id = DOM.uniqueId(); 10632 o.role= "option"; 10633 o.onclick = function() { 10634 if (t.settings.onselect(o.value) !== false) 10635 t.select(o.value); // Must be runned after 10636 }; 10637 10638 m.add(o); 10639 } 10640 }); 10641 10642 t.onRenderMenu.dispatch(t, m); 10643 t.menu = m; 10644 }, 10645 10646 postRender : function() { 10647 var t = this, cp = t.classPrefix; 10648 10649 Event.add(t.id, 'click', t.showMenu, t); 10650 Event.add(t.id, 'keydown', function(evt) { 10651 if (evt.keyCode == 32) { // Space 10652 t.showMenu(evt); 10653 Event.cancel(evt); 10654 } 10655 }); 10656 Event.add(t.id, 'focus', function() { 10657 if (!t._focused) { 10658 t.keyDownHandler = Event.add(t.id, 'keydown', function(e) { 10659 if (e.keyCode == 40) { 10660 t.showMenu(); 10661 Event.cancel(e); 10662 } 10663 }); 10664 t.keyPressHandler = Event.add(t.id, 'keypress', function(e) { 10665 var v; 10666 if (e.keyCode == 13) { 10667 // Fake select on enter 10668 v = t.selectedValue; 10669 t.selectedValue = null; // Needs to be null to fake change 10670 Event.cancel(e); 10671 t.settings.onselect(v); 10672 } 10673 }); 10674 } 10675 10676 t._focused = 1; 10677 }); 10678 Event.add(t.id, 'blur', function() { 10679 Event.remove(t.id, 'keydown', t.keyDownHandler); 10680 Event.remove(t.id, 'keypress', t.keyPressHandler); 10681 t._focused = 0; 10682 }); 10683 10684 // Old IE doesn't have hover on all elements 10685 if (tinymce.isIE6 || !DOM.boxModel) { 10686 Event.add(t.id, 'mouseover', function() { 10687 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10688 DOM.addClass(t.id, cp + 'Hover'); 10689 }); 10690 10691 Event.add(t.id, 'mouseout', function() { 10692 if (!DOM.hasClass(t.id, cp + 'Disabled')) 10693 DOM.removeClass(t.id, cp + 'Hover'); 10694 }); 10695 } 10696 10697 t.onPostRender.dispatch(t, DOM.get(t.id)); 10698 }, 10699 10700 destroy : function() { 10701 this.parent(); 10702 10703 Event.clear(this.id + '_text'); 10704 Event.clear(this.id + '_open'); 10705 } 10706 }); 10707 })(tinymce); 10708 10709 (function(tinymce) { 10710 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, Dispatcher = tinymce.util.Dispatcher, undef; 10711 10712 tinymce.create('tinymce.ui.NativeListBox:tinymce.ui.ListBox', { 10713 NativeListBox : function(id, s) { 10714 this.parent(id, s); 10715 this.classPrefix = 'mceNativeListBox'; 10716 }, 10717 10718 setDisabled : function(s) { 10719 DOM.get(this.id).disabled = s; 10720 this.setAriaProperty('disabled', s); 10721 }, 10722 10723 isDisabled : function() { 10724 return DOM.get(this.id).disabled; 10725 }, 10726 10727 select : function(va) { 10728 var t = this, fv, f; 10729 10730 if (va == undef) 10731 return t.selectByIndex(-1); 10732 10733 // Is string or number make function selector 10734 if (va && typeof(va)=="function") 10735 f = va; 10736 else { 10737 f = function(v) { 10738 return v == va; 10739 }; 10740 } 10741 10742 // Do we need to do something? 10743 if (va != t.selectedValue) { 10744 // Find item 10745 each(t.items, function(o, i) { 10746 if (f(o.value)) { 10747 fv = 1; 10748 t.selectByIndex(i); 10749 return false; 10750 } 10751 }); 10752 10753 if (!fv) 10754 t.selectByIndex(-1); 10755 } 10756 }, 10757 10758 selectByIndex : function(idx) { 10759 DOM.get(this.id).selectedIndex = idx + 1; 10760 this.selectedValue = this.items[idx] ? this.items[idx].value : null; 10761 }, 10762 10763 add : function(n, v, a) { 10764 var o, t = this; 10765 10766 a = a || {}; 10767 a.value = v; 10768 10769 if (t.isRendered()) 10770 DOM.add(DOM.get(this.id), 'option', a, n); 10771 10772 o = { 10773 title : n, 10774 value : v, 10775 attribs : a 10776 }; 10777 10778 t.items.push(o); 10779 t.onAdd.dispatch(t, o); 10780 }, 10781 10782 getLength : function() { 10783 return this.items.length; 10784 }, 10785 10786 renderHTML : function() { 10787 var h, t = this; 10788 10789 h = DOM.createHTML('option', {value : ''}, '-- ' + t.settings.title + ' --'); 10790 10791 each(t.items, function(it) { 10792 h += DOM.createHTML('option', {value : it.value}, it.title); 10793 }); 10794 10795 h = DOM.createHTML('select', {id : t.id, 'class' : 'mceNativeListBox', 'aria-labelledby': t.id + '_aria'}, h); 10796 h += DOM.createHTML('span', {id : t.id + '_aria', 'style': 'display: none'}, t.settings.title); 10797 return h; 10798 }, 10799 10800 postRender : function() { 10801 var t = this, ch, changeListenerAdded = true; 10802 10803 t.rendered = true; 10804 10805 function onChange(e) { 10806 var v = t.items[e.target.selectedIndex - 1]; 10807 10808 if (v && (v = v.value)) { 10809 t.onChange.dispatch(t, v); 10810 10811 if (t.settings.onselect) 10812 t.settings.onselect(v); 10813 } 10814 }; 10815 10816 Event.add(t.id, 'change', onChange); 10817 10818 // Accessibility keyhandler 10819 Event.add(t.id, 'keydown', function(e) { 10820 var bf; 10821 10822 Event.remove(t.id, 'change', ch); 10823 changeListenerAdded = false; 10824 10825 bf = Event.add(t.id, 'blur', function() { 10826 if (changeListenerAdded) return; 10827 changeListenerAdded = true; 10828 Event.add(t.id, 'change', onChange); 10829 Event.remove(t.id, 'blur', bf); 10830 }); 10831 10832 //prevent default left and right keys on chrome - so that the keyboard navigation is used. 10833 if (tinymce.isWebKit && (e.keyCode==37 ||e.keyCode==39)) { 10834 return Event.prevent(e); 10835 } 10836 10837 if (e.keyCode == 13 || e.keyCode == 32) { 10838 onChange(e); 10839 return Event.cancel(e); 10840 } 10841 }); 10842 10843 t.onPostRender.dispatch(t, DOM.get(t.id)); 10844 } 10845 }); 10846 })(tinymce); 10847 10848 (function(tinymce) { 10849 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10850 10851 tinymce.create('tinymce.ui.MenuButton:tinymce.ui.Button', { 10852 MenuButton : function(id, s, ed) { 10853 this.parent(id, s, ed); 10854 10855 this.onRenderMenu = new tinymce.util.Dispatcher(this); 10856 10857 s.menu_container = s.menu_container || DOM.doc.body; 10858 }, 10859 10860 showMenu : function() { 10861 var t = this, p1, p2, e = DOM.get(t.id), m; 10862 10863 if (t.isDisabled()) 10864 return; 10865 10866 if (!t.isMenuRendered) { 10867 t.renderMenu(); 10868 t.isMenuRendered = true; 10869 } 10870 10871 if (t.isMenuVisible) 10872 return t.hideMenu(); 10873 10874 p1 = DOM.getPos(t.settings.menu_container); 10875 p2 = DOM.getPos(e); 10876 10877 m = t.menu; 10878 m.settings.offset_x = p2.x; 10879 m.settings.offset_y = p2.y; 10880 m.settings.vp_offset_x = p2.x; 10881 m.settings.vp_offset_y = p2.y; 10882 m.settings.keyboard_focus = t._focused; 10883 m.showMenu(0, e.firstChild.clientHeight); 10884 10885 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 10886 t.setState('Selected', 1); 10887 10888 t.isMenuVisible = 1; 10889 }, 10890 10891 renderMenu : function() { 10892 var t = this, m; 10893 10894 m = t.settings.control_manager.createDropMenu(t.id + '_menu', { 10895 menu_line : 1, 10896 'class' : this.classPrefix + 'Menu', 10897 icons : t.settings.icons 10898 }); 10899 10900 m.onHideMenu.add(function() { 10901 t.hideMenu(); 10902 t.focus(); 10903 }); 10904 10905 t.onRenderMenu.dispatch(t, m); 10906 t.menu = m; 10907 }, 10908 10909 hideMenu : function(e) { 10910 var t = this; 10911 10912 // Prevent double toogles by canceling the mouse click event to the button 10913 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id || e.id === t.id + '_open';})) 10914 return; 10915 10916 if (!e || !DOM.getParent(e.target, '.mceMenu')) { 10917 t.setState('Selected', 0); 10918 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 10919 if (t.menu) 10920 t.menu.hideMenu(); 10921 } 10922 10923 t.isMenuVisible = 0; 10924 }, 10925 10926 postRender : function() { 10927 var t = this, s = t.settings; 10928 10929 Event.add(t.id, 'click', function() { 10930 if (!t.isDisabled()) { 10931 if (s.onclick) 10932 s.onclick(t.value); 10933 10934 t.showMenu(); 10935 } 10936 }); 10937 } 10938 }); 10939 })(tinymce); 10940 10941 (function(tinymce) { 10942 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each; 10943 10944 tinymce.create('tinymce.ui.SplitButton:tinymce.ui.MenuButton', { 10945 SplitButton : function(id, s, ed) { 10946 this.parent(id, s, ed); 10947 this.classPrefix = 'mceSplitButton'; 10948 }, 10949 10950 renderHTML : function() { 10951 var h, t = this, s = t.settings, h1; 10952 10953 h = '<tbody><tr>'; 10954 10955 if (s.image) 10956 h1 = DOM.createHTML('img ', {src : s.image, role: 'presentation', 'class' : 'mceAction ' + s['class']}); 10957 else 10958 h1 = DOM.createHTML('span', {'class' : 'mceAction ' + s['class']}, ''); 10959 10960 h1 += DOM.createHTML('span', {'class': 'mceVoiceLabel mceIconOnly', id: t.id + '_voice', style: 'display:none;'}, s.title); 10961 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_action', tabindex: '-1', href : 'javascript:;', 'class' : 'mceAction ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10962 10963 h1 = DOM.createHTML('span', {'class' : 'mceOpen ' + s['class']}, '<span style="display:none;" class="mceIconOnly" aria-hidden="true">\u25BC</span>'); 10964 h += '<td >' + DOM.createHTML('a', {role: 'button', id : t.id + '_open', tabindex: '-1', href : 'javascript:;', 'class' : 'mceOpen ' + s['class'], onclick : "return false;", onmousedown : 'return false;', title : s.title}, h1) + '</td>'; 10965 10966 h += '</tr></tbody>'; 10967 h = DOM.createHTML('table', { role: 'presentation', 'class' : 'mceSplitButton mceSplitButtonEnabled ' + s['class'], cellpadding : '0', cellspacing : '0', title : s.title}, h); 10968 return DOM.createHTML('div', {id : t.id, role: 'button', tabindex: '0', 'aria-labelledby': t.id + '_voice', 'aria-haspopup': 'true'}, h); 10969 }, 10970 10971 postRender : function() { 10972 var t = this, s = t.settings, activate; 10973 10974 if (s.onclick) { 10975 activate = function(evt) { 10976 if (!t.isDisabled()) { 10977 s.onclick(t.value); 10978 Event.cancel(evt); 10979 } 10980 }; 10981 Event.add(t.id + '_action', 'click', activate); 10982 Event.add(t.id, ['click', 'keydown'], function(evt) { 10983 var DOM_VK_SPACE = 32, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_UP = 38, DOM_VK_DOWN = 40; 10984 if ((evt.keyCode === 32 || evt.keyCode === 13 || evt.keyCode === 14) && !evt.altKey && !evt.ctrlKey && !evt.metaKey) { 10985 activate(); 10986 Event.cancel(evt); 10987 } else if (evt.type === 'click' || evt.keyCode === DOM_VK_DOWN) { 10988 t.showMenu(); 10989 Event.cancel(evt); 10990 } 10991 }); 10992 } 10993 10994 Event.add(t.id + '_open', 'click', function (evt) { 10995 t.showMenu(); 10996 Event.cancel(evt); 10997 }); 10998 Event.add([t.id, t.id + '_open'], 'focus', function() {t._focused = 1;}); 10999 Event.add([t.id, t.id + '_open'], 'blur', function() {t._focused = 0;}); 11000 11001 // Old IE doesn't have hover on all elements 11002 if (tinymce.isIE6 || !DOM.boxModel) { 11003 Event.add(t.id, 'mouseover', function() { 11004 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11005 DOM.addClass(t.id, 'mceSplitButtonHover'); 11006 }); 11007 11008 Event.add(t.id, 'mouseout', function() { 11009 if (!DOM.hasClass(t.id, 'mceSplitButtonDisabled')) 11010 DOM.removeClass(t.id, 'mceSplitButtonHover'); 11011 }); 11012 } 11013 }, 11014 11015 destroy : function() { 11016 this.parent(); 11017 11018 Event.clear(this.id + '_action'); 11019 Event.clear(this.id + '_open'); 11020 Event.clear(this.id); 11021 } 11022 }); 11023 })(tinymce); 11024 11025 (function(tinymce) { 11026 var DOM = tinymce.DOM, Event = tinymce.dom.Event, is = tinymce.is, each = tinymce.each; 11027 11028 tinymce.create('tinymce.ui.ColorSplitButton:tinymce.ui.SplitButton', { 11029 ColorSplitButton : function(id, s, ed) { 11030 var t = this; 11031 11032 t.parent(id, s, ed); 11033 11034 t.settings = s = tinymce.extend({ 11035 colors : '000000,993300,333300,003300,003366,000080,333399,333333,800000,FF6600,808000,008000,008080,0000FF,666699,808080,FF0000,FF9900,99CC00,339966,33CCCC,3366FF,800080,999999,FF00FF,FFCC00,FFFF00,00FF00,00FFFF,00CCFF,993366,C0C0C0,FF99CC,FFCC99,FFFF99,CCFFCC,CCFFFF,99CCFF,CC99FF,FFFFFF', 11036 grid_width : 8, 11037 default_color : '#888888' 11038 }, t.settings); 11039 11040 t.onShowMenu = new tinymce.util.Dispatcher(t); 11041 11042 t.onHideMenu = new tinymce.util.Dispatcher(t); 11043 11044 t.value = s.default_color; 11045 }, 11046 11047 showMenu : function() { 11048 var t = this, r, p, e, p2; 11049 11050 if (t.isDisabled()) 11051 return; 11052 11053 if (!t.isMenuRendered) { 11054 t.renderMenu(); 11055 t.isMenuRendered = true; 11056 } 11057 11058 if (t.isMenuVisible) 11059 return t.hideMenu(); 11060 11061 e = DOM.get(t.id); 11062 DOM.show(t.id + '_menu'); 11063 DOM.addClass(e, 'mceSplitButtonSelected'); 11064 p2 = DOM.getPos(e); 11065 DOM.setStyles(t.id + '_menu', { 11066 left : p2.x, 11067 top : p2.y + e.firstChild.clientHeight, 11068 zIndex : 200000 11069 }); 11070 e = 0; 11071 11072 Event.add(DOM.doc, 'mousedown', t.hideMenu, t); 11073 t.onShowMenu.dispatch(t); 11074 11075 if (t._focused) { 11076 t._keyHandler = Event.add(t.id + '_menu', 'keydown', function(e) { 11077 if (e.keyCode == 27) 11078 t.hideMenu(); 11079 }); 11080 11081 DOM.select('a', t.id + '_menu')[0].focus(); // Select first link 11082 } 11083 11084 t.keyboardNav = new tinymce.ui.KeyboardNavigation({ 11085 root: t.id + '_menu', 11086 items: DOM.select('a', t.id + '_menu'), 11087 onCancel: function() { 11088 t.hideMenu(); 11089 t.focus(); 11090 } 11091 }); 11092 11093 t.keyboardNav.focus(); 11094 t.isMenuVisible = 1; 11095 }, 11096 11097 hideMenu : function(e) { 11098 var t = this; 11099 11100 if (t.isMenuVisible) { 11101 // Prevent double toogles by canceling the mouse click event to the button 11102 if (e && e.type == "mousedown" && DOM.getParent(e.target, function(e) {return e.id === t.id + '_open';})) 11103 return; 11104 11105 if (!e || !DOM.getParent(e.target, '.mceSplitButtonMenu')) { 11106 DOM.removeClass(t.id, 'mceSplitButtonSelected'); 11107 Event.remove(DOM.doc, 'mousedown', t.hideMenu, t); 11108 Event.remove(t.id + '_menu', 'keydown', t._keyHandler); 11109 DOM.hide(t.id + '_menu'); 11110 } 11111 11112 t.isMenuVisible = 0; 11113 t.onHideMenu.dispatch(); 11114 t.keyboardNav.destroy(); 11115 } 11116 }, 11117 11118 renderMenu : function() { 11119 var t = this, m, i = 0, s = t.settings, n, tb, tr, w, context; 11120 11121 w = DOM.add(s.menu_container, 'div', {role: 'listbox', id : t.id + '_menu', 'class' : s.menu_class + ' ' + s['class'], style : 'position:absolute;left:0;top:-1000px;'}); 11122 m = DOM.add(w, 'div', {'class' : s['class'] + ' mceSplitButtonMenu'}); 11123 DOM.add(m, 'span', {'class' : 'mceMenuLine'}); 11124 11125 n = DOM.add(m, 'table', {role: 'presentation', 'class' : 'mceColorSplitMenu'}); 11126 tb = DOM.add(n, 'tbody'); 11127 11128 // Generate color grid 11129 i = 0; 11130 each(is(s.colors, 'array') ? s.colors : s.colors.split(','), function(c) { 11131 c = c.replace(/^#/, ''); 11132 11133 if (!i--) { 11134 tr = DOM.add(tb, 'tr'); 11135 i = s.grid_width - 1; 11136 } 11137 11138 n = DOM.add(tr, 'td'); 11139 var settings = { 11140 href : 'javascript:;', 11141 style : { 11142 backgroundColor : '#' + c 11143 }, 11144 'title': t.editor.getLang('colors.' + c, c), 11145 'data-mce-color' : '#' + c 11146 }; 11147 11148 // adding a proper ARIA role = button causes JAWS to read things incorrectly on IE. 11149 if (!tinymce.isIE ) { 11150 settings.role = 'option'; 11151 } 11152 11153 n = DOM.add(n, 'a', settings); 11154 11155 if (t.editor.forcedHighContrastMode) { 11156 n = DOM.add(n, 'canvas', { width: 16, height: 16, 'aria-hidden': 'true' }); 11157 if (n.getContext && (context = n.getContext("2d"))) { 11158 context.fillStyle = '#' + c; 11159 context.fillRect(0, 0, 16, 16); 11160 } else { 11161 // No point leaving a canvas element around if it's not supported for drawing on anyway. 11162 DOM.remove(n); 11163 } 11164 } 11165 }); 11166 11167 if (s.more_colors_func) { 11168 n = DOM.add(tb, 'tr'); 11169 n = DOM.add(n, 'td', {colspan : s.grid_width, 'class' : 'mceMoreColors'}); 11170 n = DOM.add(n, 'a', {role: 'option', id : t.id + '_more', href : 'javascript:;', onclick : 'return false;', 'class' : 'mceMoreColors'}, s.more_colors_title); 11171 11172 Event.add(n, 'click', function(e) { 11173 s.more_colors_func.call(s.more_colors_scope || this); 11174 return Event.cancel(e); // Cancel to fix onbeforeunload problem 11175 }); 11176 } 11177 11178 DOM.addClass(m, 'mceColorSplitMenu'); 11179 11180 // Prevent IE from scrolling and hindering click to occur #4019 11181 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); 11182 11183 Event.add(t.id + '_menu', 'click', function(e) { 11184 var c; 11185 11186 e = DOM.getParent(e.target, 'a', tb); 11187 11188 if (e && e.nodeName.toLowerCase() == 'a' && (c = e.getAttribute('data-mce-color'))) 11189 t.setColor(c); 11190 11191 return false; // Prevent IE auto save warning 11192 }); 11193 11194 return w; 11195 }, 11196 11197 setColor : function(c) { 11198 this.displayColor(c); 11199 this.hideMenu(); 11200 this.settings.onselect(c); 11201 }, 11202 11203 displayColor : function(c) { 11204 var t = this; 11205 11206 DOM.setStyle(t.id + '_preview', 'backgroundColor', c); 11207 11208 t.value = c; 11209 }, 11210 11211 postRender : function() { 11212 var t = this, id = t.id; 11213 11214 t.parent(); 11215 DOM.add(id + '_action', 'div', {id : id + '_preview', 'class' : 'mceColorPreview'}); 11216 DOM.setStyle(t.id + '_preview', 'backgroundColor', t.value); 11217 }, 11218 11219 destroy : function() { 11220 var self = this; 11221 11222 self.parent(); 11223 11224 Event.clear(self.id + '_menu'); 11225 Event.clear(self.id + '_more'); 11226 DOM.remove(self.id + '_menu'); 11227 11228 if (self.keyboardNav) { 11229 self.keyboardNav.destroy(); 11230 } 11231 } 11232 }); 11233 })(tinymce); 11234 11235 (function(tinymce) { 11236 // Shorten class names 11237 var dom = tinymce.DOM, each = tinymce.each, Event = tinymce.dom.Event; 11238 tinymce.create('tinymce.ui.ToolbarGroup:tinymce.ui.Container', { 11239 renderHTML : function() { 11240 var t = this, h = [], controls = t.controls, each = tinymce.each, settings = t.settings; 11241 11242 h.push('<div id="' + t.id + '" role="group" aria-labelledby="' + t.id + '_voice">'); 11243 //TODO: ACC test this out - adding a role = application for getting the landmarks working well. 11244 h.push("<span role='application'>"); 11245 h.push('<span id="' + t.id + '_voice" class="mceVoiceLabel" style="display:none;">' + dom.encode(settings.name) + '</span>'); 11246 each(controls, function(toolbar) { 11247 h.push(toolbar.renderHTML()); 11248 }); 11249 h.push("</span>"); 11250 h.push('</div>'); 11251 11252 return h.join(''); 11253 }, 11254 11255 focus : function() { 11256 var t = this; 11257 dom.get(t.id).focus(); 11258 }, 11259 11260 postRender : function() { 11261 var t = this, items = []; 11262 11263 each(t.controls, function(toolbar) { 11264 each (toolbar.controls, function(control) { 11265 if (control.id) { 11266 items.push(control); 11267 } 11268 }); 11269 }); 11270 11271 t.keyNav = new tinymce.ui.KeyboardNavigation({ 11272 root: t.id, 11273 items: items, 11274 onCancel: function() { 11275 //Move focus if webkit so that navigation back will read the item. 11276 if (tinymce.isWebKit) { 11277 dom.get(t.editor.id+"_ifr").focus(); 11278 } 11279 t.editor.focus(); 11280 }, 11281 excludeFromTabOrder: !t.settings.tab_focus_toolbar 11282 }); 11283 }, 11284 11285 destroy : function() { 11286 var self = this; 11287 11288 self.parent(); 11289 self.keyNav.destroy(); 11290 Event.clear(self.id); 11291 } 11292 }); 11293 })(tinymce); 11294 11295 (function(tinymce) { 11296 // Shorten class names 11297 var dom = tinymce.DOM, each = tinymce.each; 11298 tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { 11299 renderHTML : function() { 11300 var t = this, h = '', c, co, s = t.settings, i, pr, nx, cl; 11301 11302 cl = t.controls; 11303 for (i=0; i<cl.length; i++) { 11304 // Get current control, prev control, next control and if the control is a list box or not 11305 co = cl[i]; 11306 pr = cl[i - 1]; 11307 nx = cl[i + 1]; 11308 11309 // Add toolbar start 11310 if (i === 0) { 11311 c = 'mceToolbarStart'; 11312 11313 if (co.Button) 11314 c += ' mceToolbarStartButton'; 11315 else if (co.SplitButton) 11316 c += ' mceToolbarStartSplitButton'; 11317 else if (co.ListBox) 11318 c += ' mceToolbarStartListBox'; 11319 11320 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11321 } 11322 11323 // Add toolbar end before list box and after the previous button 11324 // This is to fix the o2k7 editor skins 11325 if (pr && co.ListBox) { 11326 if (pr.Button || pr.SplitButton) 11327 h += dom.createHTML('td', {'class' : 'mceToolbarEnd'}, dom.createHTML('span', null, '<!-- IE -->')); 11328 } 11329 11330 // Render control HTML 11331 11332 // IE 8 quick fix, needed to propertly generate a hit area for anchors 11333 if (dom.stdMode) 11334 h += '<td style="position: relative">' + co.renderHTML() + '</td>'; 11335 else 11336 h += '<td>' + co.renderHTML() + '</td>'; 11337 11338 // Add toolbar start after list box and before the next button 11339 // This is to fix the o2k7 editor skins 11340 if (nx && co.ListBox) { 11341 if (nx.Button || nx.SplitButton) 11342 h += dom.createHTML('td', {'class' : 'mceToolbarStart'}, dom.createHTML('span', null, '<!-- IE -->')); 11343 } 11344 } 11345 11346 c = 'mceToolbarEnd'; 11347 11348 if (co.Button) 11349 c += ' mceToolbarEndButton'; 11350 else if (co.SplitButton) 11351 c += ' mceToolbarEndSplitButton'; 11352 else if (co.ListBox) 11353 c += ' mceToolbarEndListBox'; 11354 11355 h += dom.createHTML('td', {'class' : c}, dom.createHTML('span', null, '<!-- IE -->')); 11356 11357 return dom.createHTML('table', {id : t.id, 'class' : 'mceToolbar' + (s['class'] ? ' ' + s['class'] : ''), cellpadding : '0', cellspacing : '0', align : t.settings.align || '', role: 'presentation', tabindex: '-1'}, '<tbody><tr>' + h + '</tr></tbody>'); 11358 } 11359 }); 11360 })(tinymce); 11361 11362 (function(tinymce) { 11363 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each; 11364 11365 tinymce.create('tinymce.AddOnManager', { 11366 AddOnManager : function() { 11367 var self = this; 11368 11369 self.items = []; 11370 self.urls = {}; 11371 self.lookup = {}; 11372 self.onAdd = new Dispatcher(self); 11373 }, 11374 11375 get : function(n) { 11376 if (this.lookup[n]) { 11377 return this.lookup[n].instance; 11378 } else { 11379 return undefined; 11380 } 11381 }, 11382 11383 dependencies : function(n) { 11384 var result; 11385 if (this.lookup[n]) { 11386 result = this.lookup[n].dependencies; 11387 } 11388 return result || []; 11389 }, 11390 11391 requireLangPack : function(n) { 11392 var s = tinymce.settings; 11393 11394 if (s && s.language && s.language_load !== false) 11395 tinymce.ScriptLoader.add(this.urls[n] + '/langs/' + s.language + '.js'); 11396 }, 11397 11398 add : function(id, o, dependencies) { 11399 this.items.push(o); 11400 this.lookup[id] = {instance:o, dependencies:dependencies}; 11401 this.onAdd.dispatch(this, id, o); 11402 11403 return o; 11404 }, 11405 createUrl: function(baseUrl, dep) { 11406 if (typeof dep === "object") { 11407 return dep 11408 } else { 11409 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 11410 } 11411 }, 11412 11413 addComponents: function(pluginName, scripts) { 11414 var pluginUrl = this.urls[pluginName]; 11415 tinymce.each(scripts, function(script){ 11416 tinymce.ScriptLoader.add(pluginUrl+"/"+script); 11417 }); 11418 }, 11419 11420 load : function(n, u, cb, s) { 11421 var t = this, url = u; 11422 11423 function loadDependencies() { 11424 var dependencies = t.dependencies(n); 11425 tinymce.each(dependencies, function(dep) { 11426 var newUrl = t.createUrl(u, dep); 11427 t.load(newUrl.resource, newUrl, undefined, undefined); 11428 }); 11429 if (cb) { 11430 if (s) { 11431 cb.call(s); 11432 } else { 11433 cb.call(tinymce.ScriptLoader); 11434 } 11435 } 11436 } 11437 11438 if (t.urls[n]) 11439 return; 11440 if (typeof u === "object") 11441 url = u.prefix + u.resource + u.suffix; 11442 11443 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) 11444 url = tinymce.baseURL + '/' + url; 11445 11446 t.urls[n] = url.substring(0, url.lastIndexOf('/')); 11447 11448 if (t.lookup[n]) { 11449 loadDependencies(); 11450 } else { 11451 tinymce.ScriptLoader.add(url, loadDependencies, s); 11452 } 11453 } 11454 }); 11455 11456 // Create plugin and theme managers 11457 tinymce.PluginManager = new tinymce.AddOnManager(); 11458 tinymce.ThemeManager = new tinymce.AddOnManager(); 11459 }(tinymce)); 11460 11461 (function(tinymce) { 11462 // Shorten names 11463 var each = tinymce.each, extend = tinymce.extend, 11464 DOM = tinymce.DOM, Event = tinymce.dom.Event, 11465 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11466 explode = tinymce.explode, 11467 Dispatcher = tinymce.util.Dispatcher, undef, instanceCounter = 0; 11468 11469 // Setup some URLs where the editor API is located and where the document is 11470 tinymce.documentBaseURL = window.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 11471 if (!/[\/\\]$/.test(tinymce.documentBaseURL)) 11472 tinymce.documentBaseURL += '/'; 11473 11474 tinymce.baseURL = new tinymce.util.URI(tinymce.documentBaseURL).toAbsolute(tinymce.baseURL); 11475 11476 tinymce.baseURI = new tinymce.util.URI(tinymce.baseURL); 11477 11478 // Add before unload listener 11479 // This was required since IE was leaking memory if you added and removed beforeunload listeners 11480 // with attachEvent/detatchEvent so this only adds one listener and instances can the attach to the onBeforeUnload event 11481 tinymce.onBeforeUnload = new Dispatcher(tinymce); 11482 11483 // Must be on window or IE will leak if the editor is placed in frame or iframe 11484 Event.add(window, 'beforeunload', function(e) { 11485 tinymce.onBeforeUnload.dispatch(tinymce, e); 11486 }); 11487 11488 tinymce.onAddEditor = new Dispatcher(tinymce); 11489 11490 tinymce.onRemoveEditor = new Dispatcher(tinymce); 11491 11492 tinymce.EditorManager = extend(tinymce, { 11493 editors : [], 11494 11495 i18n : {}, 11496 11497 activeEditor : null, 11498 11499 init : function(s) { 11500 var t = this, pl, sl = tinymce.ScriptLoader, e, el = [], ed; 11501 11502 function createId(elm) { 11503 var id = elm.id; 11504 11505 // Use element id, or unique name or generate a unique id 11506 if (!id) { 11507 id = elm.name; 11508 11509 if (id && !DOM.get(id)) { 11510 id = elm.name; 11511 } else { 11512 // Generate unique name 11513 id = DOM.uniqueId(); 11514 } 11515 11516 elm.setAttribute('id', id); 11517 } 11518 11519 return id; 11520 }; 11521 11522 function execCallback(se, n, s) { 11523 var f = se[n]; 11524 11525 if (!f) 11526 return; 11527 11528 if (tinymce.is(f, 'string')) { 11529 s = f.replace(/\.\w+$/, ''); 11530 s = s ? tinymce.resolve(s) : 0; 11531 f = tinymce.resolve(f); 11532 } 11533 11534 return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); 11535 }; 11536 11537 function hasClass(n, c) { 11538 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); 11539 }; 11540 11541 t.settings = s; 11542 11543 // Legacy call 11544 Event.bind(window, 'ready', function() { 11545 var l, co; 11546 11547 execCallback(s, 'onpageload'); 11548 11549 switch (s.mode) { 11550 case "exact": 11551 l = s.elements || ''; 11552 11553 if(l.length > 0) { 11554 each(explode(l), function(v) { 11555 if (DOM.get(v)) { 11556 ed = new tinymce.Editor(v, s); 11557 el.push(ed); 11558 ed.render(1); 11559 } else { 11560 each(document.forms, function(f) { 11561 each(f.elements, function(e) { 11562 if (e.name === v) { 11563 v = 'mce_editor_' + instanceCounter++; 11564 DOM.setAttrib(e, 'id', v); 11565 11566 ed = new tinymce.Editor(v, s); 11567 el.push(ed); 11568 ed.render(1); 11569 } 11570 }); 11571 }); 11572 } 11573 }); 11574 } 11575 break; 11576 11577 case "textareas": 11578 case "specific_textareas": 11579 each(DOM.select('textarea'), function(elm) { 11580 if (s.editor_deselector && hasClass(elm, s.editor_deselector)) 11581 return; 11582 11583 if (!s.editor_selector || hasClass(elm, s.editor_selector)) { 11584 ed = new tinymce.Editor(createId(elm), s); 11585 el.push(ed); 11586 ed.render(1); 11587 } 11588 }); 11589 break; 11590 11591 default: 11592 if (s.types) { 11593 // Process type specific selector 11594 each(s.types, function(type) { 11595 each(DOM.select(type.selector), function(elm) { 11596 var editor = new tinymce.Editor(createId(elm), tinymce.extend({}, s, type)); 11597 el.push(editor); 11598 editor.render(1); 11599 }); 11600 }); 11601 } else if (s.selector) { 11602 // Process global selector 11603 each(DOM.select(s.selector), function(elm) { 11604 var editor = new tinymce.Editor(createId(elm), s); 11605 el.push(editor); 11606 editor.render(1); 11607 }); 11608 } 11609 } 11610 11611 // Call onInit when all editors are initialized 11612 if (s.oninit) { 11613 l = co = 0; 11614 11615 each(el, function(ed) { 11616 co++; 11617 11618 if (!ed.initialized) { 11619 // Wait for it 11620 ed.onInit.add(function() { 11621 l++; 11622 11623 // All done 11624 if (l == co) 11625 execCallback(s, 'oninit'); 11626 }); 11627 } else 11628 l++; 11629 11630 // All done 11631 if (l == co) 11632 execCallback(s, 'oninit'); 11633 }); 11634 } 11635 }); 11636 }, 11637 11638 get : function(id) { 11639 if (id === undef) 11640 return this.editors; 11641 11642 return this.editors[id]; 11643 }, 11644 11645 getInstanceById : function(id) { 11646 return this.get(id); 11647 }, 11648 11649 add : function(editor) { 11650 var self = this, editors = self.editors; 11651 11652 // Add named and index editor instance 11653 editors[editor.id] = editor; 11654 editors.push(editor); 11655 11656 self._setActive(editor); 11657 self.onAddEditor.dispatch(self, editor); 11658 11659 11660 // Patch the tinymce.Editor instance with jQuery adapter logic 11661 if (tinymce.adapter) 11662 tinymce.adapter.patchEditor(editor); 11663 11664 11665 return editor; 11666 }, 11667 11668 remove : function(editor) { 11669 var t = this, i, editors = t.editors; 11670 11671 // Not in the collection 11672 if (!editors[editor.id]) 11673 return null; 11674 11675 delete editors[editor.id]; 11676 11677 for (i = 0; i < editors.length; i++) { 11678 if (editors[i] == editor) { 11679 editors.splice(i, 1); 11680 break; 11681 } 11682 } 11683 11684 // Select another editor since the active one was removed 11685 if (t.activeEditor == editor) 11686 t._setActive(editors[0]); 11687 11688 editor.destroy(); 11689 t.onRemoveEditor.dispatch(t, editor); 11690 11691 return editor; 11692 }, 11693 11694 execCommand : function(c, u, v) { 11695 var t = this, ed = t.get(v), w; 11696 11697 function clr() { 11698 ed.destroy(); 11699 w.detachEvent('onunload', clr); 11700 w = w.tinyMCE = w.tinymce = null; // IE leak 11701 }; 11702 11703 // Manager commands 11704 switch (c) { 11705 case "mceFocus": 11706 ed.focus(); 11707 return true; 11708 11709 case "mceAddEditor": 11710 case "mceAddControl": 11711 if (!t.get(v)) 11712 new tinymce.Editor(v, t.settings).render(); 11713 11714 return true; 11715 11716 case "mceAddFrameControl": 11717 w = v.window; 11718 11719 // Add tinyMCE global instance and tinymce namespace to specified window 11720 w.tinyMCE = tinyMCE; 11721 w.tinymce = tinymce; 11722 11723 tinymce.DOM.doc = w.document; 11724 tinymce.DOM.win = w; 11725 11726 ed = new tinymce.Editor(v.element_id, v); 11727 ed.render(); 11728 11729 // Fix IE memory leaks 11730 if (tinymce.isIE) { 11731 w.attachEvent('onunload', clr); 11732 } 11733 11734 v.page_window = null; 11735 11736 return true; 11737 11738 case "mceRemoveEditor": 11739 case "mceRemoveControl": 11740 if (ed) 11741 ed.remove(); 11742 11743 return true; 11744 11745 case 'mceToggleEditor': 11746 if (!ed) { 11747 t.execCommand('mceAddControl', 0, v); 11748 return true; 11749 } 11750 11751 if (ed.isHidden()) 11752 ed.show(); 11753 else 11754 ed.hide(); 11755 11756 return true; 11757 } 11758 11759 // Run command on active editor 11760 if (t.activeEditor) 11761 return t.activeEditor.execCommand(c, u, v); 11762 11763 return false; 11764 }, 11765 11766 execInstanceCommand : function(id, c, u, v) { 11767 var ed = this.get(id); 11768 11769 if (ed) 11770 return ed.execCommand(c, u, v); 11771 11772 return false; 11773 }, 11774 11775 triggerSave : function() { 11776 each(this.editors, function(e) { 11777 e.save(); 11778 }); 11779 }, 11780 11781 addI18n : function(p, o) { 11782 var lo, i18n = this.i18n; 11783 11784 if (!tinymce.is(p, 'string')) { 11785 each(p, function(o, lc) { 11786 each(o, function(o, g) { 11787 each(o, function(o, k) { 11788 if (g === 'common') 11789 i18n[lc + '.' + k] = o; 11790 else 11791 i18n[lc + '.' + g + '.' + k] = o; 11792 }); 11793 }); 11794 }); 11795 } else { 11796 each(o, function(o, k) { 11797 i18n[p + '.' + k] = o; 11798 }); 11799 } 11800 }, 11801 11802 // Private methods 11803 11804 _setActive : function(editor) { 11805 this.selectedInstance = this.activeEditor = editor; 11806 } 11807 }); 11808 })(tinymce); 11809 11810 (function(tinymce) { 11811 // Shorten these names 11812 var DOM = tinymce.DOM, Event = tinymce.dom.Event, extend = tinymce.extend, 11813 each = tinymce.each, isGecko = tinymce.isGecko, 11814 isIE = tinymce.isIE, isWebKit = tinymce.isWebKit, is = tinymce.is, 11815 ThemeManager = tinymce.ThemeManager, PluginManager = tinymce.PluginManager, 11816 explode = tinymce.explode; 11817 11818 tinymce.create('tinymce.Editor', { 11819 Editor : function(id, settings) { 11820 var self = this, TRUE = true; 11821 11822 self.settings = settings = extend({ 11823 id : id, 11824 language : 'en', 11825 theme : 'advanced', 11826 skin : 'default', 11827 delta_width : 0, 11828 delta_height : 0, 11829 popup_css : '', 11830 plugins : '', 11831 document_base_url : tinymce.documentBaseURL, 11832 add_form_submit_trigger : TRUE, 11833 submit_patch : TRUE, 11834 add_unload_trigger : TRUE, 11835 convert_urls : TRUE, 11836 relative_urls : TRUE, 11837 remove_script_host : TRUE, 11838 table_inline_editing : false, 11839 object_resizing : TRUE, 11840 accessibility_focus : TRUE, 11841 doctype : tinymce.isIE6 ? '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">' : '<!DOCTYPE>', // Use old doctype on IE 6 to avoid horizontal scroll 11842 visual : TRUE, 11843 font_size_style_values : 'xx-small,x-small,small,medium,large,x-large,xx-large', 11844 font_size_legacy_values : 'xx-small,small,medium,large,x-large,xx-large,300%', // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 11845 apply_source_formatting : TRUE, 11846 directionality : 'ltr', 11847 forced_root_block : 'p', 11848 hidden_input : TRUE, 11849 padd_empty_editor : TRUE, 11850 render_ui : TRUE, 11851 indentation : '30px', 11852 fix_table_elements : TRUE, 11853 inline_styles : TRUE, 11854 convert_fonts_to_spans : TRUE, 11855 indent : 'simple', 11856 indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11857 indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 11858 validate : TRUE, 11859 entity_encoding : 'named', 11860 url_converter : self.convertURL, 11861 url_converter_scope : self, 11862 ie7_compat : TRUE 11863 }, settings); 11864 11865 self.id = self.editorId = id; 11866 11867 self.isNotDirty = false; 11868 11869 self.plugins = {}; 11870 11871 self.documentBaseURI = new tinymce.util.URI(settings.document_base_url || tinymce.documentBaseURL, { 11872 base_uri : tinyMCE.baseURI 11873 }); 11874 11875 self.baseURI = tinymce.baseURI; 11876 11877 self.contentCSS = []; 11878 11879 self.contentStyles = []; 11880 11881 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 11882 self.setupEvents(); 11883 11884 // Internal command handler objects 11885 self.execCommands = {}; 11886 self.queryStateCommands = {}; 11887 self.queryValueCommands = {}; 11888 11889 // Call setup 11890 self.execCallback('setup', self); 11891 }, 11892 11893 render : function(nst) { 11894 var t = this, s = t.settings, id = t.id, sl = tinymce.ScriptLoader; 11895 11896 // Page is not loaded yet, wait for it 11897 if (!Event.domLoaded) { 11898 Event.add(window, 'ready', function() { 11899 t.render(); 11900 }); 11901 return; 11902 } 11903 11904 tinyMCE.settings = s; 11905 11906 // Element not found, then skip initialization 11907 if (!t.getElement()) 11908 return; 11909 11910 // Is a iPad/iPhone and not on iOS5, then skip initialization. We need to sniff 11911 // here since the browser says it has contentEditable support but there is no visible caret. 11912 if (tinymce.isIDevice && !tinymce.isIOS5) 11913 return; 11914 11915 // Add hidden input for non input elements inside form elements 11916 if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) 11917 DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); 11918 11919 // Hide target element early to prevent content flashing 11920 if (!s.content_editable) { 11921 t.orgVisibility = t.getElement().style.visibility; 11922 t.getElement().style.visibility = 'hidden'; 11923 } 11924 11925 if (tinymce.WindowManager) 11926 t.windowManager = new tinymce.WindowManager(t); 11927 11928 if (s.encoding == 'xml') { 11929 t.onGetContent.add(function(ed, o) { 11930 if (o.save) 11931 o.content = DOM.encode(o.content); 11932 }); 11933 } 11934 11935 if (s.add_form_submit_trigger) { 11936 t.onSubmit.addToTop(function() { 11937 if (t.initialized) { 11938 t.save(); 11939 t.isNotDirty = 1; 11940 } 11941 }); 11942 } 11943 11944 if (s.add_unload_trigger) { 11945 t._beforeUnload = tinyMCE.onBeforeUnload.add(function() { 11946 if (t.initialized && !t.destroyed && !t.isHidden()) 11947 t.save({format : 'raw', no_events : true}); 11948 }); 11949 } 11950 11951 tinymce.addUnload(t.destroy, t); 11952 11953 if (s.submit_patch) { 11954 t.onBeforeRenderUI.add(function() { 11955 var n = t.getElement().form; 11956 11957 if (!n) 11958 return; 11959 11960 // Already patched 11961 if (n._mceOldSubmit) 11962 return; 11963 11964 // Check page uses id="submit" or name="submit" for it's submit button 11965 if (!n.submit.nodeType && !n.submit.length) { 11966 t.formElement = n; 11967 n._mceOldSubmit = n.submit; 11968 n.submit = function() { 11969 // Save all instances 11970 tinymce.triggerSave(); 11971 t.isNotDirty = 1; 11972 11973 return t.formElement._mceOldSubmit(t.formElement); 11974 }; 11975 } 11976 11977 n = null; 11978 }); 11979 } 11980 11981 // Load scripts 11982 function loadScripts() { 11983 if (s.language && s.language_load !== false) 11984 sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); 11985 11986 if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) 11987 ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); 11988 11989 each(explode(s.plugins), function(p) { 11990 if (p &&!PluginManager.urls[p]) { 11991 if (p.charAt(0) == '-') { 11992 p = p.substr(1, p.length); 11993 var dependencies = PluginManager.dependencies(p); 11994 each(dependencies, function(dep) { 11995 var defaultSettings = {prefix:'plugins/', resource: dep, suffix:'/editor_plugin' + tinymce.suffix + '.js'}; 11996 dep = PluginManager.createUrl(defaultSettings, dep); 11997 PluginManager.load(dep.resource, dep); 11998 }); 11999 } else { 12000 // Skip safari plugin, since it is removed as of 3.3b1 12001 if (p == 'safari') { 12002 return; 12003 } 12004 PluginManager.load(p, {prefix:'plugins/', resource: p, suffix:'/editor_plugin' + tinymce.suffix + '.js'}); 12005 } 12006 } 12007 }); 12008 12009 // Init when que is loaded 12010 sl.loadQueue(function() { 12011 if (!t.removed) 12012 t.init(); 12013 }); 12014 }; 12015 12016 loadScripts(); 12017 }, 12018 12019 init : function() { 12020 var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; 12021 12022 tinymce.add(t); 12023 12024 s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); 12025 12026 if (s.theme) { 12027 if (typeof s.theme != "function") { 12028 s.theme = s.theme.replace(/-/, ''); 12029 o = ThemeManager.get(s.theme); 12030 t.theme = new o(); 12031 12032 if (t.theme.init) 12033 t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); 12034 } else { 12035 t.theme = s.theme; 12036 } 12037 } 12038 12039 function initPlugin(p) { 12040 var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; 12041 if (c && tinymce.inArray(initializedPlugins,p) === -1) { 12042 each(PluginManager.dependencies(p), function(dep){ 12043 initPlugin(dep); 12044 }); 12045 po = new c(t, u); 12046 12047 t.plugins[p] = po; 12048 12049 if (po.init) { 12050 po.init(t, u); 12051 initializedPlugins.push(p); 12052 } 12053 } 12054 } 12055 12056 // Create all plugins 12057 each(explode(s.plugins.replace(/\-/g, '')), initPlugin); 12058 12059 // Setup popup CSS path(s) 12060 if (s.popup_css !== false) { 12061 if (s.popup_css) 12062 s.popup_css = t.documentBaseURI.toAbsolute(s.popup_css); 12063 else 12064 s.popup_css = t.baseURI.toAbsolute("themes/" + s.theme + "/skins/" + s.skin + "/dialog.css"); 12065 } 12066 12067 if (s.popup_css_add) 12068 s.popup_css += ',' + t.documentBaseURI.toAbsolute(s.popup_css_add); 12069 12070 t.controlManager = new tinymce.ControlManager(t); 12071 12072 // Enables users to override the control factory 12073 t.onBeforeRenderUI.dispatch(t, t.controlManager); 12074 12075 // Measure box 12076 if (s.render_ui && t.theme) { 12077 t.orgDisplay = e.style.display; 12078 12079 if (typeof s.theme != "function") { 12080 w = s.width || e.style.width || e.offsetWidth; 12081 h = s.height || e.style.height || e.offsetHeight; 12082 mh = s.min_height || 100; 12083 re = /^[0-9\.]+(|px)$/i; 12084 12085 if (re.test('' + w)) 12086 w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); 12087 12088 if (re.test('' + h)) 12089 h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh); 12090 12091 // Render UI 12092 o = t.theme.renderUI({ 12093 targetNode : e, 12094 width : w, 12095 height : h, 12096 deltaWidth : s.delta_width, 12097 deltaHeight : s.delta_height 12098 }); 12099 12100 // Resize editor 12101 DOM.setStyles(o.sizeContainer || o.editorContainer, { 12102 width : w, 12103 height : h 12104 }); 12105 12106 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 12107 if (h < mh) 12108 h = mh; 12109 } else { 12110 o = s.theme(t, e); 12111 12112 // Convert element type to id:s 12113 if (o.editorContainer.nodeType) { 12114 o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent"; 12115 } 12116 12117 // Convert element type to id:s 12118 if (o.iframeContainer.nodeType) { 12119 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer"; 12120 } 12121 12122 // Use specified iframe height or the targets offsetHeight 12123 h = o.iframeHeight || e.offsetHeight; 12124 12125 // Store away the selection when it's changed to it can be restored later with a editor.focus() call 12126 if (isIE) { 12127 t.onInit.add(function(ed) { 12128 ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { 12129 ed.lastIERng = ed.selection.getRng(); 12130 }); 12131 }); 12132 } 12133 } 12134 12135 t.editorContainer = o.editorContainer; 12136 } 12137 12138 // Load specified content CSS last 12139 if (s.content_css) { 12140 each(explode(s.content_css), function(u) { 12141 t.contentCSS.push(t.documentBaseURI.toAbsolute(u)); 12142 }); 12143 } 12144 12145 // Content editable mode ends here 12146 if (s.content_editable) { 12147 e = n = o = null; // Fix IE leak 12148 return t.initContentBody(); 12149 } 12150 12151 // User specified a document.domain value 12152 if (document.domain && location.hostname != document.domain) 12153 tinymce.relaxedDomain = document.domain; 12154 12155 t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">'; 12156 12157 // We only need to override paths if we have to 12158 // IE has a bug where it remove site absolute urls to relative ones if this is specified 12159 if (s.document_base_url != tinymce.documentBaseURL) 12160 t.iframeHTML += '<base href="' + t.documentBaseURI.getURI() + '" />'; 12161 12162 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 12163 if (s.ie7_compat) 12164 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 12165 else 12166 t.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'; 12167 12168 t.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 12169 12170 // Load the CSS by injecting them into the HTML this will reduce "flicker" 12171 for (i = 0; i < t.contentCSS.length; i++) { 12172 t.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + t.contentCSS[i] + '" />'; 12173 } 12174 12175 t.contentCSS = []; 12176 12177 bi = s.body_id || 'tinymce'; 12178 if (bi.indexOf('=') != -1) { 12179 bi = t.getParam('body_id', '', 'hash'); 12180 bi = bi[t.id] || bi; 12181 } 12182 12183 bc = s.body_class || ''; 12184 if (bc.indexOf('=') != -1) { 12185 bc = t.getParam('body_class', '', 'hash'); 12186 bc = bc[t.id] || ''; 12187 } 12188 12189 t.iframeHTML += '</head><body id="' + bi + '" class="mceContentBody ' + bc + '" onload="window.parent.tinyMCE.get(\'' + t.id + '\').onLoad.dispatch();"><br></body></html>'; 12190 12191 // Domain relaxing enabled, then set document domain 12192 if (tinymce.relaxedDomain && (isIE || (tinymce.isOpera && parseFloat(opera.version()) < 11))) { 12193 // We need to write the contents here in IE since multiple writes messes up refresh button and back button 12194 u = 'javascript:(function(){document.open();document.domain="' + document.domain + '";var ed = window.parent.tinyMCE.get("' + t.id + '");document.write(ed.iframeHTML);document.close();ed.initContentBody();})()'; 12195 } 12196 12197 // Create iframe 12198 // TODO: ACC add the appropriate description on this. 12199 n = DOM.add(o.iframeContainer, 'iframe', { 12200 id : t.id + "_ifr", 12201 src : u || 'javascript:""', // Workaround for HTTPS warning in IE6/7 12202 frameBorder : '0', 12203 allowTransparency : "true", 12204 title : s.aria_label, 12205 style : { 12206 width : '100%', 12207 height : h, 12208 display : 'block' // Important for Gecko to render the iframe correctly 12209 } 12210 }); 12211 12212 t.contentAreaContainer = o.iframeContainer; 12213 12214 if (o.editorContainer) { 12215 DOM.get(o.editorContainer).style.display = t.orgDisplay; 12216 } 12217 12218 // Restore visibility on target element 12219 e.style.visibility = t.orgVisibility; 12220 12221 DOM.get(t.id).style.display = 'none'; 12222 DOM.setAttrib(t.id, 'aria-hidden', true); 12223 12224 if (!tinymce.relaxedDomain || !u) 12225 t.initContentBody(); 12226 12227 e = n = o = null; // Cleanup 12228 }, 12229 12230 initContentBody : function() { 12231 var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText; 12232 12233 // Setup iframe body 12234 if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) { 12235 doc.open(); 12236 doc.write(self.iframeHTML); 12237 doc.close(); 12238 12239 if (tinymce.relaxedDomain) 12240 doc.domain = tinymce.relaxedDomain; 12241 } 12242 12243 if (settings.content_editable) { 12244 DOM.addClass(targetElm, 'mceContentBody'); 12245 self.contentDocument = doc = settings.content_document || document; 12246 self.contentWindow = settings.content_window || window; 12247 self.bodyElement = targetElm; 12248 12249 // Prevent leak in IE 12250 settings.content_document = settings.content_window = null; 12251 } 12252 12253 // It will not steal focus while setting contentEditable 12254 body = self.getBody(); 12255 body.disabled = true; 12256 12257 if (!settings.readonly) 12258 body.contentEditable = self.getParam('content_editable_state', true); 12259 12260 body.disabled = false; 12261 12262 self.schema = new tinymce.html.Schema(settings); 12263 12264 self.dom = new tinymce.dom.DOMUtils(doc, { 12265 keep_values : true, 12266 url_converter : self.convertURL, 12267 url_converter_scope : self, 12268 hex_colors : settings.force_hex_style_colors, 12269 class_filter : settings.class_filter, 12270 update_styles : true, 12271 root_element : settings.content_editable ? self.id : null, 12272 schema : self.schema 12273 }); 12274 12275 self.parser = new tinymce.html.DomParser(settings, self.schema); 12276 12277 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 12278 self.parser.addAttributeFilter('src,href,style', function(nodes, name) { 12279 var i = nodes.length, node, dom = self.dom, value, internalName; 12280 12281 while (i--) { 12282 node = nodes[i]; 12283 value = node.attr(name); 12284 internalName = 'data-mce-' + name; 12285 12286 // Add internal attribute if we need to we don't on a refresh of the document 12287 if (!node.attributes.map[internalName]) { 12288 if (name === "style") 12289 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); 12290 else 12291 node.attr(internalName, self.convertURL(value, name, node.name)); 12292 } 12293 } 12294 }); 12295 12296 // Keep scripts from executing 12297 self.parser.addNodeFilter('script', function(nodes, name) { 12298 var i = nodes.length, node; 12299 12300 while (i--) { 12301 node = nodes[i]; 12302 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); 12303 } 12304 }); 12305 12306 self.parser.addNodeFilter('#cdata', function(nodes, name) { 12307 var i = nodes.length, node; 12308 12309 while (i--) { 12310 node = nodes[i]; 12311 node.type = 8; 12312 node.name = '#comment'; 12313 node.value = '[CDATA[' + node.value + ']]'; 12314 } 12315 }); 12316 12317 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes, name) { 12318 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 12319 12320 while (i--) { 12321 node = nodes[i]; 12322 12323 if (node.isEmpty(nonEmptyElements)) 12324 node.empty().append(new tinymce.html.Node('br', 1)).shortEnded = true; 12325 } 12326 }); 12327 12328 self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema); 12329 12330 self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self); 12331 12332 self.formatter = new tinymce.Formatter(self); 12333 12334 self.undoManager = new tinymce.UndoManager(self); 12335 12336 self.forceBlocks = new tinymce.ForceBlocks(self); 12337 self.enterKey = new tinymce.EnterKey(self); 12338 self.editorCommands = new tinymce.EditorCommands(self); 12339 12340 self.onExecCommand.add(function(editor, command) { 12341 // Don't refresh the select lists until caret move 12342 if (!/^(FontName|FontSize)$/.test(command)) 12343 self.nodeChanged(); 12344 }); 12345 12346 // Pass through 12347 self.serializer.onPreProcess.add(function(se, o) { 12348 return self.onPreProcess.dispatch(self, o, se); 12349 }); 12350 12351 self.serializer.onPostProcess.add(function(se, o) { 12352 return self.onPostProcess.dispatch(self, o, se); 12353 }); 12354 12355 self.onPreInit.dispatch(self); 12356 12357 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) 12358 doc.body.spellcheck = false; 12359 12360 if (!settings.readonly) { 12361 self.bindNativeEvents(); 12362 } 12363 12364 self.controlManager.onPostRender.dispatch(self, self.controlManager); 12365 self.onPostRender.dispatch(self); 12366 12367 self.quirks = tinymce.util.Quirks(self); 12368 12369 if (settings.directionality) 12370 body.dir = settings.directionality; 12371 12372 if (settings.nowrap) 12373 body.style.whiteSpace = "nowrap"; 12374 12375 if (settings.protect) { 12376 self.onBeforeSetContent.add(function(ed, o) { 12377 each(settings.protect, function(pattern) { 12378 o.content = o.content.replace(pattern, function(str) { 12379 return '<!--mce:protected ' + escape(str) + '-->'; 12380 }); 12381 }); 12382 }); 12383 } 12384 12385 // Add visual aids when new contents is added 12386 self.onSetContent.add(function() { 12387 self.addVisual(self.getBody()); 12388 }); 12389 12390 // Remove empty contents 12391 if (settings.padd_empty_editor) { 12392 self.onPostProcess.add(function(ed, o) { 12393 o.content = o.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 12394 }); 12395 } 12396 12397 self.load({initial : true, format : 'html'}); 12398 self.startContent = self.getContent({format : 'raw'}); 12399 12400 self.initialized = true; 12401 12402 self.onInit.dispatch(self); 12403 self.execCallback('setupcontent_callback', self.id, body, doc); 12404 self.execCallback('init_instance_callback', self); 12405 self.focus(true); 12406 self.nodeChanged({initial : true}); 12407 12408 // Add editor specific CSS styles 12409 if (self.contentStyles.length > 0) { 12410 contentCssText = ''; 12411 12412 each(self.contentStyles, function(style) { 12413 contentCssText += style + "\r\n"; 12414 }); 12415 12416 self.dom.addStyle(contentCssText); 12417 } 12418 12419 // Load specified content CSS last 12420 each(self.contentCSS, function(url) { 12421 self.dom.loadCSS(url); 12422 }); 12423 12424 // Handle auto focus 12425 if (settings.auto_focus) { 12426 setTimeout(function () { 12427 var ed = tinymce.get(settings.auto_focus); 12428 12429 ed.selection.select(ed.getBody(), 1); 12430 ed.selection.collapse(1); 12431 ed.getBody().focus(); 12432 ed.getWin().focus(); 12433 }, 100); 12434 } 12435 12436 // Clean up references for IE 12437 targetElm = doc = body = null; 12438 }, 12439 12440 focus : function(skip_focus) { 12441 var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; 12442 12443 if (!skip_focus) { 12444 if (self.lastIERng) { 12445 selection.setRng(self.lastIERng); 12446 } 12447 12448 // Get selected control element 12449 ieRng = selection.getRng(); 12450 if (ieRng.item) { 12451 controlElm = ieRng.item(0); 12452 } 12453 12454 self._refreshContentEditable(); 12455 12456 // Focus the window iframe 12457 if (!contentEditable) { 12458 self.getWin().focus(); 12459 } 12460 12461 // Focus the body as well since it's contentEditable 12462 if (tinymce.isGecko || contentEditable) { 12463 body = self.getBody(); 12464 12465 // Check for setActive since it doesn't scroll to the element 12466 if (body.setActive) { 12467 body.setActive(); 12468 } else { 12469 body.focus(); 12470 } 12471 12472 if (contentEditable) { 12473 selection.normalize(); 12474 } 12475 } 12476 12477 // Restore selected control element 12478 // This is needed when for example an image is selected within a 12479 // layer a call to focus will then remove the control selection 12480 if (controlElm && controlElm.ownerDocument == doc) { 12481 ieRng = doc.body.createControlRange(); 12482 ieRng.addElement(controlElm); 12483 ieRng.select(); 12484 } 12485 } 12486 12487 if (tinymce.activeEditor != self) { 12488 if ((oed = tinymce.activeEditor) != null) 12489 oed.onDeactivate.dispatch(oed, self); 12490 12491 self.onActivate.dispatch(self, oed); 12492 } 12493 12494 tinymce._setActive(self); 12495 }, 12496 12497 execCallback : function(n) { 12498 var t = this, f = t.settings[n], s; 12499 12500 if (!f) 12501 return; 12502 12503 // Look through lookup 12504 if (t.callbackLookup && (s = t.callbackLookup[n])) { 12505 f = s.func; 12506 s = s.scope; 12507 } 12508 12509 if (is(f, 'string')) { 12510 s = f.replace(/\.\w+$/, ''); 12511 s = s ? tinymce.resolve(s) : 0; 12512 f = tinymce.resolve(f); 12513 t.callbackLookup = t.callbackLookup || {}; 12514 t.callbackLookup[n] = {func : f, scope : s}; 12515 } 12516 12517 return f.apply(s || t, Array.prototype.slice.call(arguments, 1)); 12518 }, 12519 12520 translate : function(s) { 12521 var c = this.settings.language || 'en', i18n = tinymce.i18n; 12522 12523 if (!s) 12524 return ''; 12525 12526 return i18n[c + '.' + s] || s.replace(/\{\#([^\}]+)\}/g, function(a, b) { 12527 return i18n[c + '.' + b] || '{#' + b + '}'; 12528 }); 12529 }, 12530 12531 getLang : function(n, dv) { 12532 return tinymce.i18n[(this.settings.language || 'en') + '.' + n] || (is(dv) ? dv : '{#' + n + '}'); 12533 }, 12534 12535 getParam : function(n, dv, ty) { 12536 var tr = tinymce.trim, v = is(this.settings[n]) ? this.settings[n] : dv, o; 12537 12538 if (ty === 'hash') { 12539 o = {}; 12540 12541 if (is(v, 'string')) { 12542 each(v.indexOf('=') > 0 ? v.split(/[;,](?![^=;,]*(?:[;,]|$))/) : v.split(','), function(v) { 12543 v = v.split('='); 12544 12545 if (v.length > 1) 12546 o[tr(v[0])] = tr(v[1]); 12547 else 12548 o[tr(v[0])] = tr(v); 12549 }); 12550 } else 12551 o = v; 12552 12553 return o; 12554 } 12555 12556 return v; 12557 }, 12558 12559 nodeChanged : function(o) { 12560 var self = this, selection = self.selection, node; 12561 12562 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 12563 if (self.initialized) { 12564 o = o || {}; 12565 12566 // Get start node 12567 node = selection.getStart() || self.getBody(); 12568 node = isIE && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state 12569 12570 // Get parents and add them to object 12571 o.parents = []; 12572 self.dom.getParent(node, function(node) { 12573 if (node.nodeName == 'BODY') 12574 return true; 12575 12576 o.parents.push(node); 12577 }); 12578 12579 self.onNodeChange.dispatch( 12580 self, 12581 o ? o.controlManager || self.controlManager : self.controlManager, 12582 node, 12583 selection.isCollapsed(), 12584 o 12585 ); 12586 } 12587 }, 12588 12589 addButton : function(name, settings) { 12590 var self = this; 12591 12592 self.buttons = self.buttons || {}; 12593 self.buttons[name] = settings; 12594 }, 12595 12596 addCommand : function(name, callback, scope) { 12597 this.execCommands[name] = {func : callback, scope : scope || this}; 12598 }, 12599 12600 addQueryStateHandler : function(name, callback, scope) { 12601 this.queryStateCommands[name] = {func : callback, scope : scope || this}; 12602 }, 12603 12604 addQueryValueHandler : function(name, callback, scope) { 12605 this.queryValueCommands[name] = {func : callback, scope : scope || this}; 12606 }, 12607 12608 addShortcut : function(pa, desc, cmd_func, sc) { 12609 var t = this, c; 12610 12611 if (t.settings.custom_shortcuts === false) 12612 return false; 12613 12614 t.shortcuts = t.shortcuts || {}; 12615 12616 if (is(cmd_func, 'string')) { 12617 c = cmd_func; 12618 12619 cmd_func = function() { 12620 t.execCommand(c, false, null); 12621 }; 12622 } 12623 12624 if (is(cmd_func, 'object')) { 12625 c = cmd_func; 12626 12627 cmd_func = function() { 12628 t.execCommand(c[0], c[1], c[2]); 12629 }; 12630 } 12631 12632 each(explode(pa), function(pa) { 12633 var o = { 12634 func : cmd_func, 12635 scope : sc || this, 12636 desc : t.translate(desc), 12637 alt : false, 12638 ctrl : false, 12639 shift : false 12640 }; 12641 12642 each(explode(pa, '+'), function(v) { 12643 switch (v) { 12644 case 'alt': 12645 case 'ctrl': 12646 case 'shift': 12647 o[v] = true; 12648 break; 12649 12650 default: 12651 o.charCode = v.charCodeAt(0); 12652 o.keyCode = v.toUpperCase().charCodeAt(0); 12653 } 12654 }); 12655 12656 t.shortcuts[(o.ctrl ? 'ctrl' : '') + ',' + (o.alt ? 'alt' : '') + ',' + (o.shift ? 'shift' : '') + ',' + o.keyCode] = o; 12657 }); 12658 12659 return true; 12660 }, 12661 12662 execCommand : function(cmd, ui, val, a) { 12663 var t = this, s = 0, o, st; 12664 12665 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint|SelectAll)$/.test(cmd) && (!a || !a.skip_focus)) 12666 t.focus(); 12667 12668 a = extend({}, a); 12669 t.onBeforeExecCommand.dispatch(t, cmd, ui, val, a); 12670 if (a.terminate) 12671 return false; 12672 12673 // Command callback 12674 if (t.execCallback('execcommand_callback', t.id, t.selection.getNode(), cmd, ui, val)) { 12675 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12676 return true; 12677 } 12678 12679 // Registred commands 12680 if (o = t.execCommands[cmd]) { 12681 st = o.func.call(o.scope, ui, val); 12682 12683 // Fall through on true 12684 if (st !== true) { 12685 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12686 return st; 12687 } 12688 } 12689 12690 // Plugin commands 12691 each(t.plugins, function(p) { 12692 if (p.execCommand && p.execCommand(cmd, ui, val)) { 12693 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12694 s = 1; 12695 return false; 12696 } 12697 }); 12698 12699 if (s) 12700 return true; 12701 12702 // Theme commands 12703 if (t.theme && t.theme.execCommand && t.theme.execCommand(cmd, ui, val)) { 12704 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12705 return true; 12706 } 12707 12708 // Editor commands 12709 if (t.editorCommands.execCommand(cmd, ui, val)) { 12710 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12711 return true; 12712 } 12713 12714 // Browser commands 12715 t.getDoc().execCommand(cmd, ui, val); 12716 t.onExecCommand.dispatch(t, cmd, ui, val, a); 12717 }, 12718 12719 queryCommandState : function(cmd) { 12720 var t = this, o, s; 12721 12722 // Is hidden then return undefined 12723 if (t._isHidden()) 12724 return; 12725 12726 // Registred commands 12727 if (o = t.queryStateCommands[cmd]) { 12728 s = o.func.call(o.scope); 12729 12730 // Fall though on true 12731 if (s !== true) 12732 return s; 12733 } 12734 12735 // Registred commands 12736 o = t.editorCommands.queryCommandState(cmd); 12737 if (o !== -1) 12738 return o; 12739 12740 // Browser commands 12741 try { 12742 return this.getDoc().queryCommandState(cmd); 12743 } catch (ex) { 12744 // Fails sometimes see bug: 1896577 12745 } 12746 }, 12747 12748 queryCommandValue : function(c) { 12749 var t = this, o, s; 12750 12751 // Is hidden then return undefined 12752 if (t._isHidden()) 12753 return; 12754 12755 // Registred commands 12756 if (o = t.queryValueCommands[c]) { 12757 s = o.func.call(o.scope); 12758 12759 // Fall though on true 12760 if (s !== true) 12761 return s; 12762 } 12763 12764 // Registred commands 12765 o = t.editorCommands.queryCommandValue(c); 12766 if (is(o)) 12767 return o; 12768 12769 // Browser commands 12770 try { 12771 return this.getDoc().queryCommandValue(c); 12772 } catch (ex) { 12773 // Fails sometimes see bug: 1896577 12774 } 12775 }, 12776 12777 show : function() { 12778 var self = this; 12779 12780 DOM.show(self.getContainer()); 12781 DOM.hide(self.id); 12782 self.load(); 12783 }, 12784 12785 hide : function() { 12786 var self = this, doc = self.getDoc(); 12787 12788 // Fixed bug where IE has a blinking cursor left from the editor 12789 if (isIE && doc) 12790 doc.execCommand('SelectAll'); 12791 12792 // We must save before we hide so Safari doesn't crash 12793 self.save(); 12794 DOM.hide(self.getContainer()); 12795 DOM.setStyle(self.id, 'display', self.orgDisplay); 12796 }, 12797 12798 isHidden : function() { 12799 return !DOM.isHidden(this.id); 12800 }, 12801 12802 setProgressState : function(b, ti, o) { 12803 this.onSetProgressState.dispatch(this, b, ti, o); 12804 12805 return b; 12806 }, 12807 12808 load : function(o) { 12809 var t = this, e = t.getElement(), h; 12810 12811 if (e) { 12812 o = o || {}; 12813 o.load = true; 12814 12815 // Double encode existing entities in the value 12816 h = t.setContent(is(e.value) ? e.value : e.innerHTML, o); 12817 o.element = e; 12818 12819 if (!o.no_events) 12820 t.onLoadContent.dispatch(t, o); 12821 12822 o.element = e = null; 12823 12824 return h; 12825 } 12826 }, 12827 12828 save : function(o) { 12829 var t = this, e = t.getElement(), h, f; 12830 12831 if (!e || !t.initialized) 12832 return; 12833 12834 o = o || {}; 12835 o.save = true; 12836 12837 o.element = e; 12838 h = o.content = t.getContent(o); 12839 12840 if (!o.no_events) 12841 t.onSaveContent.dispatch(t, o); 12842 12843 h = o.content; 12844 12845 if (!/TEXTAREA|INPUT/i.test(e.nodeName)) { 12846 e.innerHTML = h; 12847 12848 // Update hidden form element 12849 if (f = DOM.getParent(t.id, 'form')) { 12850 each(f.elements, function(e) { 12851 if (e.name == t.id) { 12852 e.value = h; 12853 return false; 12854 } 12855 }); 12856 } 12857 } else 12858 e.value = h; 12859 12860 o.element = e = null; 12861 12862 return h; 12863 }, 12864 12865 setContent : function(content, args) { 12866 var self = this, rootNode, body = self.getBody(), forcedRootBlockName; 12867 12868 // Setup args object 12869 args = args || {}; 12870 args.format = args.format || 'html'; 12871 args.set = true; 12872 args.content = content; 12873 12874 // Do preprocessing 12875 if (!args.no_events) 12876 self.onBeforeSetContent.dispatch(self, args); 12877 12878 content = args.content; 12879 12880 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 12881 // It will also be impossible to place the caret in the editor unless there is a BR element present 12882 if (!tinymce.isIE && (content.length === 0 || /^\s+$/.test(content))) { 12883 forcedRootBlockName = self.settings.forced_root_block; 12884 if (forcedRootBlockName) 12885 content = '<' + forcedRootBlockName + '><br data-mce-bogus="1"></' + forcedRootBlockName + '>'; 12886 else 12887 content = '<br data-mce-bogus="1">'; 12888 12889 body.innerHTML = content; 12890 self.selection.select(body, true); 12891 self.selection.collapse(true); 12892 return; 12893 } 12894 12895 // Parse and serialize the html 12896 if (args.format !== 'raw') { 12897 content = new tinymce.html.Serializer({}, self.schema).serialize( 12898 self.parser.parse(content) 12899 ); 12900 } 12901 12902 // Set the new cleaned contents to the editor 12903 args.content = tinymce.trim(content); 12904 self.dom.setHTML(body, args.content); 12905 12906 // Do post processing 12907 if (!args.no_events) 12908 self.onSetContent.dispatch(self, args); 12909 12910 // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise 12911 if (!self.settings.content_editable || document.activeElement === self.getBody()) { 12912 self.selection.normalize(); 12913 } 12914 12915 return args.content; 12916 }, 12917 12918 getContent : function(args) { 12919 var self = this, content; 12920 12921 // Setup args object 12922 args = args || {}; 12923 args.format = args.format || 'html'; 12924 args.get = true; 12925 args.getInner = true; 12926 12927 // Do preprocessing 12928 if (!args.no_events) 12929 self.onBeforeGetContent.dispatch(self, args); 12930 12931 // Get raw contents or by default the cleaned contents 12932 if (args.format == 'raw') 12933 content = self.getBody().innerHTML; 12934 else 12935 content = self.serializer.serialize(self.getBody(), args); 12936 12937 args.content = tinymce.trim(content); 12938 12939 // Do post processing 12940 if (!args.no_events) 12941 self.onGetContent.dispatch(self, args); 12942 12943 return args.content; 12944 }, 12945 12946 isDirty : function() { 12947 var self = this; 12948 12949 return tinymce.trim(self.startContent) != tinymce.trim(self.getContent({format : 'raw', no_events : 1})) && !self.isNotDirty; 12950 }, 12951 12952 getContainer : function() { 12953 var self = this; 12954 12955 if (!self.container) 12956 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 12957 12958 return self.container; 12959 }, 12960 12961 getContentAreaContainer : function() { 12962 return this.contentAreaContainer; 12963 }, 12964 12965 getElement : function() { 12966 return DOM.get(this.settings.content_element || this.id); 12967 }, 12968 12969 getWin : function() { 12970 var self = this, elm; 12971 12972 if (!self.contentWindow) { 12973 elm = DOM.get(self.id + "_ifr"); 12974 12975 if (elm) 12976 self.contentWindow = elm.contentWindow; 12977 } 12978 12979 return self.contentWindow; 12980 }, 12981 12982 getDoc : function() { 12983 var self = this, win; 12984 12985 if (!self.contentDocument) { 12986 win = self.getWin(); 12987 12988 if (win) 12989 self.contentDocument = win.document; 12990 } 12991 12992 return self.contentDocument; 12993 }, 12994 12995 getBody : function() { 12996 return this.bodyElement || this.getDoc().body; 12997 }, 12998 12999 convertURL : function(url, name, elm) { 13000 var self = this, settings = self.settings; 13001 13002 // Use callback instead 13003 if (settings.urlconverter_callback) 13004 return self.execCallback('urlconverter_callback', url, elm, true, name); 13005 13006 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 13007 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0) 13008 return url; 13009 13010 // Convert to relative 13011 if (settings.relative_urls) 13012 return self.documentBaseURI.toRelative(url); 13013 13014 // Convert to absolute 13015 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 13016 13017 return url; 13018 }, 13019 13020 addVisual : function(elm) { 13021 var self = this, settings = self.settings, dom = self.dom, cls; 13022 13023 elm = elm || self.getBody(); 13024 13025 if (!is(self.hasVisual)) 13026 self.hasVisual = settings.visual; 13027 13028 each(dom.select('table,a', elm), function(elm) { 13029 var value; 13030 13031 switch (elm.nodeName) { 13032 case 'TABLE': 13033 cls = settings.visual_table_class || 'mceItemTable'; 13034 value = dom.getAttrib(elm, 'border'); 13035 13036 if (!value || value == '0') { 13037 if (self.hasVisual) 13038 dom.addClass(elm, cls); 13039 else 13040 dom.removeClass(elm, cls); 13041 } 13042 13043 return; 13044 13045 case 'A': 13046 if (!dom.getAttrib(elm, 'href', false)) { 13047 value = dom.getAttrib(elm, 'name') || elm.id; 13048 cls = 'mceItemAnchor'; 13049 13050 if (value) { 13051 if (self.hasVisual) 13052 dom.addClass(elm, cls); 13053 else 13054 dom.removeClass(elm, cls); 13055 } 13056 } 13057 13058 return; 13059 } 13060 }); 13061 13062 self.onVisualAid.dispatch(self, elm, self.hasVisual); 13063 }, 13064 13065 remove : function() { 13066 var self = this, elm = self.getContainer(); 13067 13068 if (!self.removed) { 13069 self.removed = 1; // Cancels post remove event execution 13070 self.hide(); 13071 13072 // Don't clear the window or document if content editable 13073 // is enabled since other instances might still be present 13074 if (!self.settings.content_editable) { 13075 Event.unbind(self.getWin()); 13076 Event.unbind(self.getDoc()); 13077 } 13078 13079 Event.unbind(self.getBody()); 13080 Event.clear(elm); 13081 13082 self.execCallback('remove_instance_callback', self); 13083 self.onRemove.dispatch(self); 13084 13085 // Clear all execCommand listeners this is required to avoid errors if the editor was removed inside another command 13086 self.onExecCommand.listeners = []; 13087 13088 tinymce.remove(self); 13089 DOM.remove(elm); 13090 } 13091 }, 13092 13093 destroy : function(s) { 13094 var t = this; 13095 13096 // One time is enough 13097 if (t.destroyed) 13098 return; 13099 13100 // We must unbind on Gecko since it would otherwise produce the pesky "attempt to run compile-and-go script on a cleared scope" message 13101 if (isGecko) { 13102 Event.unbind(t.getDoc()); 13103 Event.unbind(t.getWin()); 13104 Event.unbind(t.getBody()); 13105 } 13106 13107 if (!s) { 13108 tinymce.removeUnload(t.destroy); 13109 tinyMCE.onBeforeUnload.remove(t._beforeUnload); 13110 13111 // Manual destroy 13112 if (t.theme && t.theme.destroy) 13113 t.theme.destroy(); 13114 13115 // Destroy controls, selection and dom 13116 t.controlManager.destroy(); 13117 t.selection.destroy(); 13118 t.dom.destroy(); 13119 } 13120 13121 if (t.formElement) { 13122 t.formElement.submit = t.formElement._mceOldSubmit; 13123 t.formElement._mceOldSubmit = null; 13124 } 13125 13126 t.contentAreaContainer = t.formElement = t.container = t.settings.content_element = t.bodyElement = t.contentDocument = t.contentWindow = null; 13127 13128 if (t.selection) 13129 t.selection = t.selection.win = t.selection.dom = t.selection.dom.doc = null; 13130 13131 t.destroyed = 1; 13132 }, 13133 13134 // Internal functions 13135 13136 _refreshContentEditable : function() { 13137 var self = this, body, parent; 13138 13139 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 13140 if (self._isHidden()) { 13141 body = self.getBody(); 13142 parent = body.parentNode; 13143 13144 parent.removeChild(body); 13145 parent.appendChild(body); 13146 13147 body.focus(); 13148 } 13149 }, 13150 13151 _isHidden : function() { 13152 var s; 13153 13154 if (!isGecko) 13155 return 0; 13156 13157 // Weird, wheres that cursor selection? 13158 s = this.selection.getSel(); 13159 return (!s || !s.rangeCount || s.rangeCount === 0); 13160 } 13161 }); 13162 })(tinymce); 13163 (function(tinymce) { 13164 var each = tinymce.each; 13165 13166 tinymce.Editor.prototype.setupEvents = function() { 13167 var self = this, settings = self.settings; 13168 13169 // Add events to the editor 13170 each([ 13171 'onPreInit', 13172 13173 'onBeforeRenderUI', 13174 13175 'onPostRender', 13176 13177 'onLoad', 13178 13179 'onInit', 13180 13181 'onRemove', 13182 13183 'onActivate', 13184 13185 'onDeactivate', 13186 13187 'onClick', 13188 13189 'onEvent', 13190 13191 'onMouseUp', 13192 13193 'onMouseDown', 13194 13195 'onDblClick', 13196 13197 'onKeyDown', 13198 13199 'onKeyUp', 13200 13201 'onKeyPress', 13202 13203 'onContextMenu', 13204 13205 'onSubmit', 13206 13207 'onReset', 13208 13209 'onPaste', 13210 13211 'onPreProcess', 13212 13213 'onPostProcess', 13214 13215 'onBeforeSetContent', 13216 13217 'onBeforeGetContent', 13218 13219 'onSetContent', 13220 13221 'onGetContent', 13222 13223 'onLoadContent', 13224 13225 'onSaveContent', 13226 13227 'onNodeChange', 13228 13229 'onChange', 13230 13231 'onBeforeExecCommand', 13232 13233 'onExecCommand', 13234 13235 'onUndo', 13236 13237 'onRedo', 13238 13239 'onVisualAid', 13240 13241 'onSetProgressState', 13242 13243 'onSetAttrib' 13244 ], function(name) { 13245 self[name] = new tinymce.util.Dispatcher(self); 13246 }); 13247 13248 // Handle legacy cleanup_callback option 13249 if (settings.cleanup_callback) { 13250 self.onBeforeSetContent.add(function(ed, o) { 13251 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13252 }); 13253 13254 self.onPreProcess.add(function(ed, o) { 13255 if (o.set) 13256 ed.execCallback('cleanup_callback', 'insert_to_editor_dom', o.node, o); 13257 13258 if (o.get) 13259 ed.execCallback('cleanup_callback', 'get_from_editor_dom', o.node, o); 13260 }); 13261 13262 self.onPostProcess.add(function(ed, o) { 13263 if (o.set) 13264 o.content = ed.execCallback('cleanup_callback', 'insert_to_editor', o.content, o); 13265 13266 if (o.get) 13267 o.content = ed.execCallback('cleanup_callback', 'get_from_editor', o.content, o); 13268 }); 13269 } 13270 13271 // Handle legacy save_callback option 13272 if (settings.save_callback) { 13273 self.onGetContent.add(function(ed, o) { 13274 if (o.save) 13275 o.content = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13276 }); 13277 } 13278 13279 // Handle legacy handle_event_callback option 13280 if (settings.handle_event_callback) { 13281 self.onEvent.add(function(ed, e, o) { 13282 if (self.execCallback('handle_event_callback', e, ed, o) === false) { 13283 e.preventDefault(); 13284 e.stopPropagation(); 13285 } 13286 }); 13287 } 13288 13289 // Handle legacy handle_node_change_callback option 13290 if (settings.handle_node_change_callback) { 13291 self.onNodeChange.add(function(ed, cm, n) { 13292 ed.execCallback('handle_node_change_callback', ed.id, n, -1, -1, true, ed.selection.isCollapsed()); 13293 }); 13294 } 13295 13296 // Handle legacy save_callback option 13297 if (settings.save_callback) { 13298 self.onSaveContent.add(function(ed, o) { 13299 var h = ed.execCallback('save_callback', ed.id, o.content, ed.getBody()); 13300 13301 if (h) 13302 o.content = h; 13303 }); 13304 } 13305 13306 // Handle legacy onchange_callback option 13307 if (settings.onchange_callback) { 13308 self.onChange.add(function(ed, l) { 13309 ed.execCallback('onchange_callback', ed, l); 13310 }); 13311 } 13312 }; 13313 13314 tinymce.Editor.prototype.bindNativeEvents = function() { 13315 // 'focus', 'blur', 'dblclick', 'beforedeactivate', submit, reset 13316 var self = this, i, settings = self.settings, dom = self.dom, nativeToDispatcherMap; 13317 13318 nativeToDispatcherMap = { 13319 mouseup : 'onMouseUp', 13320 mousedown : 'onMouseDown', 13321 click : 'onClick', 13322 keyup : 'onKeyUp', 13323 keydown : 'onKeyDown', 13324 keypress : 'onKeyPress', 13325 submit : 'onSubmit', 13326 reset : 'onReset', 13327 contextmenu : 'onContextMenu', 13328 dblclick : 'onDblClick', 13329 paste : 'onPaste' // Doesn't work in all browsers yet 13330 }; 13331 13332 // Handler that takes a native event and sends it out to a dispatcher like onKeyDown 13333 function eventHandler(evt, args) { 13334 var type = evt.type; 13335 13336 // Don't fire events when it's removed 13337 if (self.removed) 13338 return; 13339 13340 // Sends the native event out to a global dispatcher then to the specific event dispatcher 13341 if (self.onEvent.dispatch(self, evt, args) !== false) { 13342 self[nativeToDispatcherMap[evt.fakeType || evt.type]].dispatch(self, evt, args); 13343 } 13344 }; 13345 13346 // Opera doesn't support focus event for contentEditable elements so we need to fake it 13347 function doOperaFocus(e) { 13348 self.focus(true); 13349 }; 13350 13351 function nodeChanged(ed, e) { 13352 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 13353 if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) { 13354 self.selection.normalize(); 13355 } 13356 13357 self.nodeChanged(); 13358 } 13359 13360 // Add DOM events 13361 each(nativeToDispatcherMap, function(dispatcherName, nativeName) { 13362 var root = settings.content_editable ? self.getBody() : self.getDoc(); 13363 13364 switch (nativeName) { 13365 case 'contextmenu': 13366 dom.bind(root, nativeName, eventHandler); 13367 break; 13368 13369 case 'paste': 13370 dom.bind(self.getBody(), nativeName, eventHandler); 13371 break; 13372 13373 case 'submit': 13374 case 'reset': 13375 dom.bind(self.getElement().form || tinymce.DOM.getParent(self.id, 'form'), nativeName, eventHandler); 13376 break; 13377 13378 default: 13379 dom.bind(root, nativeName, eventHandler); 13380 } 13381 }); 13382 13383 // Set the editor as active when focused 13384 dom.bind(settings.content_editable ? self.getBody() : (tinymce.isGecko ? self.getDoc() : self.getWin()), 'focus', function(e) { 13385 self.focus(true); 13386 }); 13387 13388 if (settings.content_editable && tinymce.isOpera) { 13389 dom.bind(self.getBody(), 'click', doOperaFocus); 13390 dom.bind(self.getBody(), 'keydown', doOperaFocus); 13391 } 13392 13393 // Add node change handler 13394 self.onMouseUp.add(nodeChanged); 13395 13396 self.onKeyUp.add(function(ed, e) { 13397 var keyCode = e.keyCode; 13398 13399 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey) 13400 nodeChanged(ed, e); 13401 }); 13402 13403 // Add reset handler 13404 self.onReset.add(function() { 13405 self.setContent(self.startContent, {format : 'raw'}); 13406 }); 13407 13408 // Add shortcuts 13409 function handleShortcut(e, execute) { 13410 if (e.altKey || e.ctrlKey || e.metaKey) { 13411 each(self.shortcuts, function(shortcut) { 13412 var ctrlState = tinymce.isMac ? e.metaKey : e.ctrlKey; 13413 13414 if (shortcut.ctrl != ctrlState || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) 13415 return; 13416 13417 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 13418 e.preventDefault(); 13419 13420 if (execute) { 13421 shortcut.func.call(shortcut.scope); 13422 } 13423 13424 return true; 13425 } 13426 }); 13427 } 13428 }; 13429 13430 self.onKeyUp.add(function(ed, e) { 13431 handleShortcut(e); 13432 }); 13433 13434 self.onKeyPress.add(function(ed, e) { 13435 handleShortcut(e); 13436 }); 13437 13438 self.onKeyDown.add(function(ed, e) { 13439 handleShortcut(e, true); 13440 }); 13441 13442 if (tinymce.isOpera) { 13443 self.onClick.add(function(ed, e) { 13444 e.preventDefault(); 13445 }); 13446 } 13447 }; 13448 })(tinymce); 13449 (function(tinymce) { 13450 // Added for compression purposes 13451 var each = tinymce.each, undef, TRUE = true, FALSE = false; 13452 13453 tinymce.EditorCommands = function(editor) { 13454 var dom = editor.dom, 13455 selection = editor.selection, 13456 commands = {state: {}, exec : {}, value : {}}, 13457 settings = editor.settings, 13458 formatter = editor.formatter, 13459 bookmark; 13460 13461 function execCommand(command, ui, value) { 13462 var func; 13463 13464 command = command.toLowerCase(); 13465 if (func = commands.exec[command]) { 13466 func(command, ui, value); 13467 return TRUE; 13468 } 13469 13470 return FALSE; 13471 }; 13472 13473 function queryCommandState(command) { 13474 var func; 13475 13476 command = command.toLowerCase(); 13477 if (func = commands.state[command]) 13478 return func(command); 13479 13480 return -1; 13481 }; 13482 13483 function queryCommandValue(command) { 13484 var func; 13485 13486 command = command.toLowerCase(); 13487 if (func = commands.value[command]) 13488 return func(command); 13489 13490 return FALSE; 13491 }; 13492 13493 function addCommands(command_list, type) { 13494 type = type || 'exec'; 13495 13496 each(command_list, function(callback, command) { 13497 each(command.toLowerCase().split(','), function(command) { 13498 commands[type][command] = callback; 13499 }); 13500 }); 13501 }; 13502 13503 // Expose public methods 13504 tinymce.extend(this, { 13505 execCommand : execCommand, 13506 queryCommandState : queryCommandState, 13507 queryCommandValue : queryCommandValue, 13508 addCommands : addCommands 13509 }); 13510 13511 // Private methods 13512 13513 function execNativeCommand(command, ui, value) { 13514 if (ui === undef) 13515 ui = FALSE; 13516 13517 if (value === undef) 13518 value = null; 13519 13520 return editor.getDoc().execCommand(command, ui, value); 13521 }; 13522 13523 function isFormatMatch(name) { 13524 return formatter.match(name); 13525 }; 13526 13527 function toggleFormat(name, value) { 13528 formatter.toggle(name, value ? {value : value} : undef); 13529 }; 13530 13531 function storeSelection(type) { 13532 bookmark = selection.getBookmark(type); 13533 }; 13534 13535 function restoreSelection() { 13536 selection.moveToBookmark(bookmark); 13537 }; 13538 13539 // Add execCommand overrides 13540 addCommands({ 13541 // Ignore these, added for compatibility 13542 'mceResetDesignMode,mceBeginUndoLevel' : function() {}, 13543 13544 // Add undo manager logic 13545 'mceEndUndoLevel,mceAddUndoLevel' : function() { 13546 editor.undoManager.add(); 13547 }, 13548 13549 'Cut,Copy,Paste' : function(command) { 13550 var doc = editor.getDoc(), failed; 13551 13552 // Try executing the native command 13553 try { 13554 execNativeCommand(command); 13555 } catch (ex) { 13556 // Command failed 13557 failed = TRUE; 13558 } 13559 13560 // Present alert message about clipboard access not being available 13561 if (failed || !doc.queryCommandSupported(command)) { 13562 if (tinymce.isGecko) { 13563 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) { 13564 if (state) 13565 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank'); 13566 }); 13567 } else 13568 editor.windowManager.alert(editor.getLang('clipboard_no_support')); 13569 } 13570 }, 13571 13572 // Override unlink command 13573 unlink : function(command) { 13574 if (selection.isCollapsed()) 13575 selection.select(selection.getNode()); 13576 13577 execNativeCommand(command); 13578 selection.collapse(FALSE); 13579 }, 13580 13581 // Override justify commands to use the text formatter engine 13582 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13583 var align = command.substring(7); 13584 13585 // Remove all other alignments first 13586 each('left,center,right,full'.split(','), function(name) { 13587 if (align != name) 13588 formatter.remove('align' + name); 13589 }); 13590 13591 toggleFormat('align' + align); 13592 execCommand('mceRepaint'); 13593 }, 13594 13595 // Override list commands to fix WebKit bug 13596 'InsertUnorderedList,InsertOrderedList' : function(command) { 13597 var listElm, listParent; 13598 13599 execNativeCommand(command); 13600 13601 // WebKit produces lists within block elements so we need to split them 13602 // we will replace the native list creation logic to custom logic later on 13603 // TODO: Remove this when the list creation logic is removed 13604 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 13605 if (listElm) { 13606 listParent = listElm.parentNode; 13607 13608 // If list is within a text block then split that block 13609 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 13610 storeSelection(); 13611 dom.split(listParent, listElm); 13612 restoreSelection(); 13613 } 13614 } 13615 }, 13616 13617 // Override commands to use the text formatter engine 13618 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13619 toggleFormat(command); 13620 }, 13621 13622 // Override commands to use the text formatter engine 13623 'ForeColor,HiliteColor,FontName' : function(command, ui, value) { 13624 toggleFormat(command, value); 13625 }, 13626 13627 FontSize : function(command, ui, value) { 13628 var fontClasses, fontSizes; 13629 13630 // Convert font size 1-7 to styles 13631 if (value >= 1 && value <= 7) { 13632 fontSizes = tinymce.explode(settings.font_size_style_values); 13633 fontClasses = tinymce.explode(settings.font_size_classes); 13634 13635 if (fontClasses) 13636 value = fontClasses[value - 1] || value; 13637 else 13638 value = fontSizes[value - 1] || value; 13639 } 13640 13641 toggleFormat(command, value); 13642 }, 13643 13644 RemoveFormat : function(command) { 13645 formatter.remove(command); 13646 }, 13647 13648 mceBlockQuote : function(command) { 13649 toggleFormat('blockquote'); 13650 }, 13651 13652 FormatBlock : function(command, ui, value) { 13653 return toggleFormat(value || 'p'); 13654 }, 13655 13656 mceCleanup : function() { 13657 var bookmark = selection.getBookmark(); 13658 13659 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE}); 13660 13661 selection.moveToBookmark(bookmark); 13662 }, 13663 13664 mceRemoveNode : function(command, ui, value) { 13665 var node = value || selection.getNode(); 13666 13667 // Make sure that the body node isn't removed 13668 if (node != editor.getBody()) { 13669 storeSelection(); 13670 editor.dom.remove(node, TRUE); 13671 restoreSelection(); 13672 } 13673 }, 13674 13675 mceSelectNodeDepth : function(command, ui, value) { 13676 var counter = 0; 13677 13678 dom.getParent(selection.getNode(), function(node) { 13679 if (node.nodeType == 1 && counter++ == value) { 13680 selection.select(node); 13681 return FALSE; 13682 } 13683 }, editor.getBody()); 13684 }, 13685 13686 mceSelectNode : function(command, ui, value) { 13687 selection.select(value); 13688 }, 13689 13690 mceInsertContent : function(command, ui, value) { 13691 var parser, serializer, parentNode, rootNode, fragment, args, 13692 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement; 13693 13694 //selection.normalize(); 13695 13696 // Setup parser and serializer 13697 parser = editor.parser; 13698 serializer = new tinymce.html.Serializer({}, editor.schema); 13699 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>'; 13700 13701 // Run beforeSetContent handlers on the HTML to be inserted 13702 args = {content: value, format: 'html'}; 13703 selection.onBeforeSetContent.dispatch(selection, args); 13704 value = args.content; 13705 13706 // Add caret at end of contents if it's missing 13707 if (value.indexOf('{$caret}') == -1) 13708 value += '{$caret}'; 13709 13710 // Replace the caret marker with a span bookmark element 13711 value = value.replace(/\{\$caret\}/, bookmarkHtml); 13712 13713 // Insert node maker where we will insert the new HTML and get it's parent 13714 if (!selection.isCollapsed()) 13715 editor.getDoc().execCommand('Delete', false, null); 13716 13717 parentNode = selection.getNode(); 13718 13719 // Parse the fragment within the context of the parent node 13720 args = {context : parentNode.nodeName.toLowerCase()}; 13721 fragment = parser.parse(value, args); 13722 13723 // Move the caret to a more suitable location 13724 node = fragment.lastChild; 13725 if (node.attr('id') == 'mce_marker') { 13726 marker = node; 13727 13728 for (node = node.prev; node; node = node.walk(true)) { 13729 if (node.type == 3 || !dom.isBlock(node.name)) { 13730 node.parent.insert(marker, node, node.name === 'br'); 13731 break; 13732 } 13733 } 13734 } 13735 13736 // If parser says valid we can insert the contents into that parent 13737 if (!args.invalid) { 13738 value = serializer.serialize(fragment); 13739 13740 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 13741 node = parentNode.firstChild; 13742 node2 = parentNode.lastChild; 13743 if (!node || (node === node2 && node.nodeName === 'BR')) 13744 dom.setHTML(parentNode, value); 13745 else 13746 selection.setContent(value); 13747 } else { 13748 // If the fragment was invalid within that context then we need 13749 // to parse and process the parent it's inserted into 13750 13751 // Insert bookmark node and get the parent 13752 selection.setContent(bookmarkHtml); 13753 parentNode = editor.selection.getNode(); 13754 rootNode = editor.getBody(); 13755 13756 // Opera will return the document node when selection is in root 13757 if (parentNode.nodeType == 9) 13758 parentNode = node = rootNode; 13759 else 13760 node = parentNode; 13761 13762 // Find the ancestor just before the root element 13763 while (node !== rootNode) { 13764 parentNode = node; 13765 node = node.parentNode; 13766 } 13767 13768 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 13769 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 13770 value = serializer.serialize( 13771 parser.parse( 13772 // Need to replace by using a function since $ in the contents would otherwise be a problem 13773 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 13774 return serializer.serialize(fragment); 13775 }) 13776 ) 13777 ); 13778 13779 // Set the inner/outer HTML depending on if we are in the root or not 13780 if (parentNode == rootNode) 13781 dom.setHTML(rootNode, value); 13782 else 13783 dom.setOuterHTML(parentNode, value); 13784 } 13785 13786 marker = dom.get('mce_marker'); 13787 13788 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well 13789 nodeRect = dom.getRect(marker); 13790 viewPortRect = dom.getViewPort(editor.getWin()); 13791 13792 // Check if node is out side the viewport if it is then scroll to it 13793 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) || 13794 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) { 13795 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody(); 13796 viewportBodyElement.scrollLeft = nodeRect.x; 13797 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25; 13798 } 13799 13800 // Move selection before marker and remove it 13801 rng = dom.createRng(); 13802 13803 // If previous sibling is a text node set the selection to the end of that node 13804 node = marker.previousSibling; 13805 if (node && node.nodeType == 3) { 13806 rng.setStart(node, node.nodeValue.length); 13807 } else { 13808 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 13809 rng.setStartBefore(marker); 13810 rng.setEndBefore(marker); 13811 } 13812 13813 // Remove the marker node and set the new range 13814 dom.remove(marker); 13815 selection.setRng(rng); 13816 13817 // Dispatch after event and add any visual elements needed 13818 selection.onSetContent.dispatch(selection, args); 13819 editor.addVisual(); 13820 }, 13821 13822 mceInsertRawHTML : function(command, ui, value) { 13823 selection.setContent('tiny_mce_marker'); 13824 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); 13825 }, 13826 13827 mceToggleFormat : function(command, ui, value) { 13828 toggleFormat(value); 13829 }, 13830 13831 mceSetContent : function(command, ui, value) { 13832 editor.setContent(value); 13833 }, 13834 13835 'Indent,Outdent' : function(command) { 13836 var intentValue, indentUnit, value; 13837 13838 // Setup indent level 13839 intentValue = settings.indentation; 13840 indentUnit = /[a-z%]+$/i.exec(intentValue); 13841 intentValue = parseInt(intentValue); 13842 13843 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 13844 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 13845 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 13846 formatter.apply('div'); 13847 } 13848 13849 each(selection.getSelectedBlocks(), function(element) { 13850 if (command == 'outdent') { 13851 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue); 13852 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : ''); 13853 } else 13854 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit); 13855 }); 13856 } else 13857 execNativeCommand(command); 13858 }, 13859 13860 mceRepaint : function() { 13861 var bookmark; 13862 13863 if (tinymce.isGecko) { 13864 try { 13865 storeSelection(TRUE); 13866 13867 if (selection.getSel()) 13868 selection.getSel().selectAllChildren(editor.getBody()); 13869 13870 selection.collapse(TRUE); 13871 restoreSelection(); 13872 } catch (ex) { 13873 // Ignore 13874 } 13875 } 13876 }, 13877 13878 mceToggleFormat : function(command, ui, value) { 13879 formatter.toggle(value); 13880 }, 13881 13882 InsertHorizontalRule : function() { 13883 editor.execCommand('mceInsertContent', false, '<hr />'); 13884 }, 13885 13886 mceToggleVisualAid : function() { 13887 editor.hasVisual = !editor.hasVisual; 13888 editor.addVisual(); 13889 }, 13890 13891 mceReplaceContent : function(command, ui, value) { 13892 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'}))); 13893 }, 13894 13895 mceInsertLink : function(command, ui, value) { 13896 var anchor; 13897 13898 if (typeof(value) == 'string') 13899 value = {href : value}; 13900 13901 anchor = dom.getParent(selection.getNode(), 'a'); 13902 13903 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 13904 value.href = value.href.replace(' ', '%20'); 13905 13906 // Remove existing links if there could be child links or that the href isn't specified 13907 if (!anchor || !value.href) { 13908 formatter.remove('link'); 13909 } 13910 13911 // Apply new link to selection 13912 if (value.href) { 13913 formatter.apply('link', value, anchor); 13914 } 13915 }, 13916 13917 selectAll : function() { 13918 var root = dom.getRoot(), rng = dom.createRng(); 13919 13920 rng.setStart(root, 0); 13921 rng.setEnd(root, root.childNodes.length); 13922 13923 editor.selection.setRng(rng); 13924 } 13925 }); 13926 13927 // Add queryCommandState overrides 13928 addCommands({ 13929 // Override justify commands 13930 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) { 13931 var name = 'align' + command.substring(7); 13932 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 13933 var matches = tinymce.map(nodes, function(node) { 13934 return !!formatter.matchNode(node, name); 13935 }); 13936 return tinymce.inArray(matches, TRUE) !== -1; 13937 }, 13938 13939 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) { 13940 return isFormatMatch(command); 13941 }, 13942 13943 mceBlockQuote : function() { 13944 return isFormatMatch('blockquote'); 13945 }, 13946 13947 Outdent : function() { 13948 var node; 13949 13950 if (settings.inline_styles) { 13951 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13952 return TRUE; 13953 13954 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0) 13955 return TRUE; 13956 } 13957 13958 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')); 13959 }, 13960 13961 'InsertUnorderedList,InsertOrderedList' : function(command) { 13962 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); 13963 } 13964 }, 'state'); 13965 13966 // Add queryCommandValue overrides 13967 addCommands({ 13968 'FontSize,FontName' : function(command) { 13969 var value = 0, parent; 13970 13971 if (parent = dom.getParent(selection.getNode(), 'span')) { 13972 if (command == 'fontsize') 13973 value = parent.style.fontSize; 13974 else 13975 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 13976 } 13977 13978 return value; 13979 } 13980 }, 'value'); 13981 13982 // Add undo manager logic 13983 addCommands({ 13984 Undo : function() { 13985 editor.undoManager.undo(); 13986 }, 13987 13988 Redo : function() { 13989 editor.undoManager.redo(); 13990 } 13991 }); 13992 }; 13993 })(tinymce); 13994 13995 (function(tinymce) { 13996 var Dispatcher = tinymce.util.Dispatcher; 13997 13998 tinymce.UndoManager = function(editor) { 13999 var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo; 14000 14001 function getContent() { 14002 // Remove whitespace before/after and remove pure bogus nodes 14003 return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, '')); 14004 }; 14005 14006 function addNonTypingUndoLevel() { 14007 self.typing = false; 14008 self.add(); 14009 }; 14010 14011 // Create event instances 14012 onBeforeAdd = new Dispatcher(self); 14013 onAdd = new Dispatcher(self); 14014 onUndo = new Dispatcher(self); 14015 onRedo = new Dispatcher(self); 14016 14017 // Pass though onAdd event from UndoManager to Editor as onChange 14018 onAdd.add(function(undoman, level) { 14019 if (undoman.hasUndo()) 14020 return editor.onChange.dispatch(editor, level, undoman); 14021 }); 14022 14023 // Pass though onUndo event from UndoManager to Editor 14024 onUndo.add(function(undoman, level) { 14025 return editor.onUndo.dispatch(editor, level, undoman); 14026 }); 14027 14028 // Pass though onRedo event from UndoManager to Editor 14029 onRedo.add(function(undoman, level) { 14030 return editor.onRedo.dispatch(editor, level, undoman); 14031 }); 14032 14033 // Add initial undo level when the editor is initialized 14034 editor.onInit.add(function() { 14035 self.add(); 14036 }); 14037 14038 // Get position before an execCommand is processed 14039 editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) { 14040 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14041 self.beforeChange(); 14042 } 14043 }); 14044 14045 // Add undo level after an execCommand call was made 14046 editor.onExecCommand.add(function(ed, cmd, ui, val, args) { 14047 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) { 14048 self.add(); 14049 } 14050 }); 14051 14052 // Add undo level on save contents, drag end and blur/focusout 14053 editor.onSaveContent.add(addNonTypingUndoLevel); 14054 editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel); 14055 editor.dom.bind(editor.getDoc(), tinymce.isGecko ? 'blur' : 'focusout', function(e) { 14056 if (!editor.removed && self.typing) { 14057 addNonTypingUndoLevel(); 14058 } 14059 }); 14060 14061 editor.onKeyUp.add(function(editor, e) { 14062 var keyCode = e.keyCode; 14063 14064 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 14065 addNonTypingUndoLevel(); 14066 } 14067 }); 14068 14069 editor.onKeyDown.add(function(editor, e) { 14070 var keyCode = e.keyCode; 14071 14072 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 14073 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 14074 if (self.typing) { 14075 addNonTypingUndoLevel(); 14076 } 14077 14078 return; 14079 } 14080 14081 // If key isn't shift,ctrl,alt,capslock,metakey 14082 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 14083 self.beforeChange(); 14084 self.typing = true; 14085 self.add(); 14086 } 14087 }); 14088 14089 editor.onMouseDown.add(function(editor, e) { 14090 if (self.typing) { 14091 addNonTypingUndoLevel(); 14092 } 14093 }); 14094 14095 // Add keyboard shortcuts for undo/redo keys 14096 editor.addShortcut('ctrl+z', 'undo_desc', 'Undo'); 14097 editor.addShortcut('ctrl+y', 'redo_desc', 'Redo'); 14098 14099 self = { 14100 // Explose for debugging reasons 14101 data : data, 14102 14103 typing : false, 14104 14105 onBeforeAdd: onBeforeAdd, 14106 14107 onAdd : onAdd, 14108 14109 onUndo : onUndo, 14110 14111 onRedo : onRedo, 14112 14113 beforeChange : function() { 14114 beforeBookmark = editor.selection.getBookmark(2, true); 14115 }, 14116 14117 add : function(level) { 14118 var i, settings = editor.settings, lastLevel; 14119 14120 level = level || {}; 14121 level.content = getContent(); 14122 14123 self.onBeforeAdd.dispatch(self, level); 14124 14125 // Add undo level if needed 14126 lastLevel = data[index]; 14127 if (lastLevel && lastLevel.content == level.content) 14128 return null; 14129 14130 // Set before bookmark on previous level 14131 if (data[index]) 14132 data[index].beforeBookmark = beforeBookmark; 14133 14134 // Time to compress 14135 if (settings.custom_undo_redo_levels) { 14136 if (data.length > settings.custom_undo_redo_levels) { 14137 for (i = 0; i < data.length - 1; i++) 14138 data[i] = data[i + 1]; 14139 14140 data.length--; 14141 index = data.length; 14142 } 14143 } 14144 14145 // Get a non intrusive normalized bookmark 14146 level.bookmark = editor.selection.getBookmark(2, true); 14147 14148 // Crop array if needed 14149 if (index < data.length - 1) 14150 data.length = index + 1; 14151 14152 data.push(level); 14153 index = data.length - 1; 14154 14155 self.onAdd.dispatch(self, level); 14156 editor.isNotDirty = 0; 14157 14158 return level; 14159 }, 14160 14161 undo : function() { 14162 var level, i; 14163 14164 if (self.typing) { 14165 self.add(); 14166 self.typing = false; 14167 } 14168 14169 if (index > 0) { 14170 level = data[--index]; 14171 14172 editor.setContent(level.content, {format : 'raw'}); 14173 editor.selection.moveToBookmark(level.beforeBookmark); 14174 14175 self.onUndo.dispatch(self, level); 14176 } 14177 14178 return level; 14179 }, 14180 14181 redo : function() { 14182 var level; 14183 14184 if (index < data.length - 1) { 14185 level = data[++index]; 14186 14187 editor.setContent(level.content, {format : 'raw'}); 14188 editor.selection.moveToBookmark(level.bookmark); 14189 14190 self.onRedo.dispatch(self, level); 14191 } 14192 14193 return level; 14194 }, 14195 14196 clear : function() { 14197 data = []; 14198 index = 0; 14199 self.typing = false; 14200 }, 14201 14202 hasUndo : function() { 14203 return index > 0 || this.typing; 14204 }, 14205 14206 hasRedo : function() { 14207 return index < data.length - 1 && !this.typing; 14208 } 14209 }; 14210 14211 return self; 14212 }; 14213 })(tinymce); 14214 14215 tinymce.ForceBlocks = function(editor) { 14216 var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); 14217 14218 function addRootBlocks() { 14219 var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument; 14220 14221 if (!node || node.nodeType !== 1 || !settings.forced_root_block) 14222 return; 14223 14224 // Check if node is wrapped in block 14225 while (node && node != rootNode) { 14226 if (blockElements[node.nodeName]) 14227 return; 14228 14229 node = node.parentNode; 14230 } 14231 14232 // Get current selection 14233 rng = selection.getRng(); 14234 if (rng.setStart) { 14235 startContainer = rng.startContainer; 14236 startOffset = rng.startOffset; 14237 endContainer = rng.endContainer; 14238 endOffset = rng.endOffset; 14239 } else { 14240 // Force control range into text range 14241 if (rng.item) { 14242 node = rng.item(0); 14243 rng = editor.getDoc().body.createTextRange(); 14244 rng.moveToElementText(node); 14245 } 14246 14247 isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc(); 14248 tmpRng = rng.duplicate(); 14249 tmpRng.collapse(true); 14250 startOffset = tmpRng.move('character', offset) * -1; 14251 14252 if (!tmpRng.collapsed) { 14253 tmpRng = rng.duplicate(); 14254 tmpRng.collapse(false); 14255 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 14256 } 14257 } 14258 14259 // Wrap non block elements and text nodes 14260 node = rootNode.firstChild; 14261 while (node) { 14262 if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { 14263 if (!rootBlockNode) { 14264 rootBlockNode = dom.create(settings.forced_root_block); 14265 node.parentNode.insertBefore(rootBlockNode, node); 14266 wrapped = true; 14267 } 14268 14269 tempNode = node; 14270 node = node.nextSibling; 14271 rootBlockNode.appendChild(tempNode); 14272 } else { 14273 rootBlockNode = null; 14274 node = node.nextSibling; 14275 } 14276 } 14277 14278 if (wrapped) { 14279 if (rng.setStart) { 14280 rng.setStart(startContainer, startOffset); 14281 rng.setEnd(endContainer, endOffset); 14282 selection.setRng(rng); 14283 } else { 14284 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 14285 if (isInEditorDocument) { 14286 try { 14287 rng = editor.getDoc().body.createTextRange(); 14288 rng.moveToElementText(rootNode); 14289 rng.collapse(true); 14290 rng.moveStart('character', startOffset); 14291 14292 if (endOffset > 0) 14293 rng.moveEnd('character', endOffset); 14294 14295 rng.select(); 14296 } catch (ex) { 14297 // Ignore 14298 } 14299 } 14300 } 14301 14302 editor.nodeChanged(); 14303 } 14304 }; 14305 14306 // Force root blocks 14307 if (settings.forced_root_block) { 14308 editor.onKeyUp.add(addRootBlocks); 14309 editor.onNodeChange.add(addRootBlocks); 14310 } 14311 }; 14312 14313 (function(tinymce) { 14314 // Shorten names 14315 var DOM = tinymce.DOM, Event = tinymce.dom.Event, each = tinymce.each, extend = tinymce.extend; 14316 14317 tinymce.create('tinymce.ControlManager', { 14318 ControlManager : function(ed, s) { 14319 var t = this, i; 14320 14321 s = s || {}; 14322 t.editor = ed; 14323 t.controls = {}; 14324 t.onAdd = new tinymce.util.Dispatcher(t); 14325 t.onPostRender = new tinymce.util.Dispatcher(t); 14326 t.prefix = s.prefix || ed.id + '_'; 14327 t._cls = {}; 14328 14329 t.onPostRender.add(function() { 14330 each(t.controls, function(c) { 14331 c.postRender(); 14332 }); 14333 }); 14334 }, 14335 14336 get : function(id) { 14337 return this.controls[this.prefix + id] || this.controls[id]; 14338 }, 14339 14340 setActive : function(id, s) { 14341 var c = null; 14342 14343 if (c = this.get(id)) 14344 c.setActive(s); 14345 14346 return c; 14347 }, 14348 14349 setDisabled : function(id, s) { 14350 var c = null; 14351 14352 if (c = this.get(id)) 14353 c.setDisabled(s); 14354 14355 return c; 14356 }, 14357 14358 add : function(c) { 14359 var t = this; 14360 14361 if (c) { 14362 t.controls[c.id] = c; 14363 t.onAdd.dispatch(c, t); 14364 } 14365 14366 return c; 14367 }, 14368 14369 createControl : function(name) { 14370 var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName; 14371 14372 // Build control factory cache 14373 if (!self.controlFactories) { 14374 self.controlFactories = []; 14375 each(editor.plugins, function(plugin) { 14376 if (plugin.createControl) { 14377 self.controlFactories.push(plugin); 14378 } 14379 }); 14380 } 14381 14382 // Create controls by asking cached factories 14383 factories = self.controlFactories; 14384 for (i = 0, l = factories.length; i < l; i++) { 14385 ctrl = factories[i].createControl(name, self); 14386 14387 if (ctrl) { 14388 return self.add(ctrl); 14389 } 14390 } 14391 14392 // Create sepearator 14393 if (name === "|" || name === "separator") { 14394 return self.createSeparator(); 14395 } 14396 14397 // Create control from button collection 14398 if (editor.buttons && (ctrl = editor.buttons[name])) { 14399 return self.createButton(name, ctrl); 14400 } 14401 14402 return self.add(ctrl); 14403 }, 14404 14405 createDropMenu : function(id, s, cc) { 14406 var t = this, ed = t.editor, c, bm, v, cls; 14407 14408 s = extend({ 14409 'class' : 'mceDropDown', 14410 constrain : ed.settings.constrain_menus 14411 }, s); 14412 14413 s['class'] = s['class'] + ' ' + ed.getParam('skin') + 'Skin'; 14414 if (v = ed.getParam('skin_variant')) 14415 s['class'] += ' ' + ed.getParam('skin') + 'Skin' + v.substring(0, 1).toUpperCase() + v.substring(1); 14416 14417 s['class'] += ed.settings.directionality == "rtl" ? ' mceRtl' : ''; 14418 14419 id = t.prefix + id; 14420 cls = cc || t._cls.dropmenu || tinymce.ui.DropMenu; 14421 c = t.controls[id] = new cls(id, s); 14422 c.onAddItem.add(function(c, o) { 14423 var s = o.settings; 14424 14425 s.title = ed.getLang(s.title, s.title); 14426 14427 if (!s.onclick) { 14428 s.onclick = function(v) { 14429 if (s.cmd) 14430 ed.execCommand(s.cmd, s.ui || false, s.value); 14431 }; 14432 } 14433 }); 14434 14435 ed.onRemove.add(function() { 14436 c.destroy(); 14437 }); 14438 14439 // Fix for bug #1897785, #1898007 14440 if (tinymce.isIE) { 14441 c.onShowMenu.add(function() { 14442 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14443 ed.focus(); 14444 14445 bm = ed.selection.getBookmark(1); 14446 }); 14447 14448 c.onHideMenu.add(function() { 14449 if (bm) { 14450 ed.selection.moveToBookmark(bm); 14451 bm = 0; 14452 } 14453 }); 14454 } 14455 14456 return t.add(c); 14457 }, 14458 14459 createListBox : function(id, s, cc) { 14460 var t = this, ed = t.editor, cmd, c, cls; 14461 14462 if (t.get(id)) 14463 return null; 14464 14465 s.title = ed.translate(s.title); 14466 s.scope = s.scope || ed; 14467 14468 if (!s.onselect) { 14469 s.onselect = function(v) { 14470 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14471 }; 14472 } 14473 14474 s = extend({ 14475 title : s.title, 14476 'class' : 'mce_' + id, 14477 scope : s.scope, 14478 control_manager : t 14479 }, s); 14480 14481 id = t.prefix + id; 14482 14483 14484 function useNativeListForAccessibility(ed) { 14485 return ed.settings.use_accessible_selects && !tinymce.isGecko 14486 } 14487 14488 if (ed.settings.use_native_selects || useNativeListForAccessibility(ed)) 14489 c = new tinymce.ui.NativeListBox(id, s); 14490 else { 14491 cls = cc || t._cls.listbox || tinymce.ui.ListBox; 14492 c = new cls(id, s, ed); 14493 } 14494 14495 t.controls[id] = c; 14496 14497 // Fix focus problem in Safari 14498 if (tinymce.isWebKit) { 14499 c.onPostRender.add(function(c, n) { 14500 // Store bookmark on mousedown 14501 Event.add(n, 'mousedown', function() { 14502 ed.bookmark = ed.selection.getBookmark(1); 14503 }); 14504 14505 // Restore on focus, since it might be lost 14506 Event.add(n, 'focus', function() { 14507 ed.selection.moveToBookmark(ed.bookmark); 14508 ed.bookmark = null; 14509 }); 14510 }); 14511 } 14512 14513 if (c.hideMenu) 14514 ed.onMouseDown.add(c.hideMenu, c); 14515 14516 return t.add(c); 14517 }, 14518 14519 createButton : function(id, s, cc) { 14520 var t = this, ed = t.editor, o, c, cls; 14521 14522 if (t.get(id)) 14523 return null; 14524 14525 s.title = ed.translate(s.title); 14526 s.label = ed.translate(s.label); 14527 s.scope = s.scope || ed; 14528 14529 if (!s.onclick && !s.menu_button) { 14530 s.onclick = function() { 14531 ed.execCommand(s.cmd, s.ui || false, s.value); 14532 }; 14533 } 14534 14535 s = extend({ 14536 title : s.title, 14537 'class' : 'mce_' + id, 14538 unavailable_prefix : ed.getLang('unavailable', ''), 14539 scope : s.scope, 14540 control_manager : t 14541 }, s); 14542 14543 id = t.prefix + id; 14544 14545 if (s.menu_button) { 14546 cls = cc || t._cls.menubutton || tinymce.ui.MenuButton; 14547 c = new cls(id, s, ed); 14548 ed.onMouseDown.add(c.hideMenu, c); 14549 } else { 14550 cls = t._cls.button || tinymce.ui.Button; 14551 c = new cls(id, s, ed); 14552 } 14553 14554 return t.add(c); 14555 }, 14556 14557 createMenuButton : function(id, s, cc) { 14558 s = s || {}; 14559 s.menu_button = 1; 14560 14561 return this.createButton(id, s, cc); 14562 }, 14563 14564 createSplitButton : function(id, s, cc) { 14565 var t = this, ed = t.editor, cmd, c, cls; 14566 14567 if (t.get(id)) 14568 return null; 14569 14570 s.title = ed.translate(s.title); 14571 s.scope = s.scope || ed; 14572 14573 if (!s.onclick) { 14574 s.onclick = function(v) { 14575 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14576 }; 14577 } 14578 14579 if (!s.onselect) { 14580 s.onselect = function(v) { 14581 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14582 }; 14583 } 14584 14585 s = extend({ 14586 title : s.title, 14587 'class' : 'mce_' + id, 14588 scope : s.scope, 14589 control_manager : t 14590 }, s); 14591 14592 id = t.prefix + id; 14593 cls = cc || t._cls.splitbutton || tinymce.ui.SplitButton; 14594 c = t.add(new cls(id, s, ed)); 14595 ed.onMouseDown.add(c.hideMenu, c); 14596 14597 return c; 14598 }, 14599 14600 createColorSplitButton : function(id, s, cc) { 14601 var t = this, ed = t.editor, cmd, c, cls, bm; 14602 14603 if (t.get(id)) 14604 return null; 14605 14606 s.title = ed.translate(s.title); 14607 s.scope = s.scope || ed; 14608 14609 if (!s.onclick) { 14610 s.onclick = function(v) { 14611 if (tinymce.isIE) 14612 bm = ed.selection.getBookmark(1); 14613 14614 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14615 }; 14616 } 14617 14618 if (!s.onselect) { 14619 s.onselect = function(v) { 14620 ed.execCommand(s.cmd, s.ui || false, v || s.value); 14621 }; 14622 } 14623 14624 s = extend({ 14625 title : s.title, 14626 'class' : 'mce_' + id, 14627 'menu_class' : ed.getParam('skin') + 'Skin', 14628 scope : s.scope, 14629 more_colors_title : ed.getLang('more_colors') 14630 }, s); 14631 14632 id = t.prefix + id; 14633 cls = cc || t._cls.colorsplitbutton || tinymce.ui.ColorSplitButton; 14634 c = new cls(id, s, ed); 14635 ed.onMouseDown.add(c.hideMenu, c); 14636 14637 // Remove the menu element when the editor is removed 14638 ed.onRemove.add(function() { 14639 c.destroy(); 14640 }); 14641 14642 // Fix for bug #1897785, #1898007 14643 if (tinymce.isIE) { 14644 c.onShowMenu.add(function() { 14645 // IE 8 needs focus in order to store away a range with the current collapsed caret location 14646 ed.focus(); 14647 bm = ed.selection.getBookmark(1); 14648 }); 14649 14650 c.onHideMenu.add(function() { 14651 if (bm) { 14652 ed.selection.moveToBookmark(bm); 14653 bm = 0; 14654 } 14655 }); 14656 } 14657 14658 return t.add(c); 14659 }, 14660 14661 createToolbar : function(id, s, cc) { 14662 var c, t = this, cls; 14663 14664 id = t.prefix + id; 14665 cls = cc || t._cls.toolbar || tinymce.ui.Toolbar; 14666 c = new cls(id, s, t.editor); 14667 14668 if (t.get(id)) 14669 return null; 14670 14671 return t.add(c); 14672 }, 14673 14674 createToolbarGroup : function(id, s, cc) { 14675 var c, t = this, cls; 14676 id = t.prefix + id; 14677 cls = cc || this._cls.toolbarGroup || tinymce.ui.ToolbarGroup; 14678 c = new cls(id, s, t.editor); 14679 14680 if (t.get(id)) 14681 return null; 14682 14683 return t.add(c); 14684 }, 14685 14686 createSeparator : function(cc) { 14687 var cls = cc || this._cls.separator || tinymce.ui.Separator; 14688 14689 return new cls(); 14690 }, 14691 14692 setControlType : function(n, c) { 14693 return this._cls[n.toLowerCase()] = c; 14694 }, 14695 14696 destroy : function() { 14697 each(this.controls, function(c) { 14698 c.destroy(); 14699 }); 14700 14701 this.controls = null; 14702 } 14703 }); 14704 })(tinymce); 14705 14706 (function(tinymce) { 14707 var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera; 14708 14709 tinymce.create('tinymce.WindowManager', { 14710 WindowManager : function(ed) { 14711 var t = this; 14712 14713 t.editor = ed; 14714 t.onOpen = new Dispatcher(t); 14715 t.onClose = new Dispatcher(t); 14716 t.params = {}; 14717 t.features = {}; 14718 }, 14719 14720 open : function(s, p) { 14721 var t = this, f = '', x, y, mo = t.editor.settings.dialog_type == 'modal', w, sw, sh, vp = tinymce.DOM.getViewPort(), u; 14722 14723 // Default some options 14724 s = s || {}; 14725 p = p || {}; 14726 sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window 14727 sh = isOpera ? vp.h : screen.height; 14728 s.name = s.name || 'mc_' + new Date().getTime(); 14729 s.width = parseInt(s.width || 320); 14730 s.height = parseInt(s.height || 240); 14731 s.resizable = true; 14732 s.left = s.left || parseInt(sw / 2.0) - (s.width / 2.0); 14733 s.top = s.top || parseInt(sh / 2.0) - (s.height / 2.0); 14734 p.inline = false; 14735 p.mce_width = s.width; 14736 p.mce_height = s.height; 14737 p.mce_auto_focus = s.auto_focus; 14738 14739 if (mo) { 14740 if (isIE) { 14741 s.center = true; 14742 s.help = false; 14743 s.dialogWidth = s.width + 'px'; 14744 s.dialogHeight = s.height + 'px'; 14745 s.scroll = s.scrollbars || false; 14746 } 14747 } 14748 14749 // Build features string 14750 each(s, function(v, k) { 14751 if (tinymce.is(v, 'boolean')) 14752 v = v ? 'yes' : 'no'; 14753 14754 if (!/^(name|url)$/.test(k)) { 14755 if (isIE && mo) 14756 f += (f ? ';' : '') + k + ':' + v; 14757 else 14758 f += (f ? ',' : '') + k + '=' + v; 14759 } 14760 }); 14761 14762 t.features = s; 14763 t.params = p; 14764 t.onOpen.dispatch(t, s, p); 14765 14766 u = s.url || s.file; 14767 u = tinymce._addVer(u); 14768 14769 try { 14770 if (isIE && mo) { 14771 w = 1; 14772 window.showModalDialog(u, window, f); 14773 } else 14774 w = window.open(u, s.name, f); 14775 } catch (ex) { 14776 // Ignore 14777 } 14778 14779 if (!w) 14780 alert(t.editor.getLang('popup_blocked')); 14781 }, 14782 14783 close : function(w) { 14784 w.close(); 14785 this.onClose.dispatch(this); 14786 }, 14787 14788 createInstance : function(cl, a, b, c, d, e) { 14789 var f = tinymce.resolve(cl); 14790 14791 return new f(a, b, c, d, e); 14792 }, 14793 14794 confirm : function(t, cb, s, w) { 14795 w = w || window; 14796 14797 cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t)))); 14798 }, 14799 14800 alert : function(tx, cb, s, w) { 14801 var t = this; 14802 14803 w = w || window; 14804 w.alert(t._decode(t.editor.getLang(tx, tx))); 14805 14806 if (cb) 14807 cb.call(s || t); 14808 }, 14809 14810 resizeBy : function(dw, dh, win) { 14811 win.resizeBy(dw, dh); 14812 }, 14813 14814 // Internal functions 14815 14816 _decode : function(s) { 14817 return tinymce.DOM.decode(s).replace(/\\n/g, '\n'); 14818 } 14819 }); 14820 }(tinymce)); 14821 (function(tinymce) { 14822 tinymce.Formatter = function(ed) { 14823 var formats = {}, 14824 each = tinymce.each, 14825 dom = ed.dom, 14826 selection = ed.selection, 14827 TreeWalker = tinymce.dom.TreeWalker, 14828 rangeUtils = new tinymce.dom.RangeUtils(dom), 14829 isValid = ed.schema.isValidChild, 14830 isBlock = dom.isBlock, 14831 forcedRootBlock = ed.settings.forced_root_block, 14832 nodeIndex = dom.nodeIndex, 14833 INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', 14834 MCE_ATTR_RE = /^(src|href|style)$/, 14835 FALSE = false, 14836 TRUE = true, 14837 formatChangeData, 14838 undef, 14839 getContentEditable = dom.getContentEditable; 14840 14841 function isArray(obj) { 14842 return obj instanceof Array; 14843 }; 14844 14845 function getParents(node, selector) { 14846 return dom.getParents(node, selector, dom.getRoot()); 14847 }; 14848 14849 function isCaretNode(node) { 14850 return node.nodeType === 1 && node.id === '_mce_caret'; 14851 }; 14852 14853 function defaultFormats() { 14854 register({ 14855 alignleft : [ 14856 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'left'}, defaultBlock: 'div'}, 14857 {selector : 'img,table', collapsed : false, styles : {'float' : 'left'}} 14858 ], 14859 14860 aligncenter : [ 14861 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'center'}, defaultBlock: 'div'}, 14862 {selector : 'img', collapsed : false, styles : {display : 'block', marginLeft : 'auto', marginRight : 'auto'}}, 14863 {selector : 'table', collapsed : false, styles : {marginLeft : 'auto', marginRight : 'auto'}} 14864 ], 14865 14866 alignright : [ 14867 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'right'}, defaultBlock: 'div'}, 14868 {selector : 'img,table', collapsed : false, styles : {'float' : 'right'}} 14869 ], 14870 14871 alignfull : [ 14872 {selector : 'figure,p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li', styles : {textAlign : 'justify'}, defaultBlock: 'div'} 14873 ], 14874 14875 bold : [ 14876 {inline : 'strong', remove : 'all'}, 14877 {inline : 'span', styles : {fontWeight : 'bold'}}, 14878 {inline : 'b', remove : 'all'} 14879 ], 14880 14881 italic : [ 14882 {inline : 'em', remove : 'all'}, 14883 {inline : 'span', styles : {fontStyle : 'italic'}}, 14884 {inline : 'i', remove : 'all'} 14885 ], 14886 14887 underline : [ 14888 {inline : 'span', styles : {textDecoration : 'underline'}, exact : true}, 14889 {inline : 'u', remove : 'all'} 14890 ], 14891 14892 strikethrough : [ 14893 {inline : 'span', styles : {textDecoration : 'line-through'}, exact : true}, 14894 {inline : 'strike', remove : 'all'} 14895 ], 14896 14897 forecolor : {inline : 'span', styles : {color : '%value'}, wrap_links : false}, 14898 hilitecolor : {inline : 'span', styles : {backgroundColor : '%value'}, wrap_links : false}, 14899 fontname : {inline : 'span', styles : {fontFamily : '%value'}}, 14900 fontsize : {inline : 'span', styles : {fontSize : '%value'}}, 14901 fontsize_class : {inline : 'span', attributes : {'class' : '%value'}}, 14902 blockquote : {block : 'blockquote', wrapper : 1, remove : 'all'}, 14903 subscript : {inline : 'sub'}, 14904 superscript : {inline : 'sup'}, 14905 14906 link : {inline : 'a', selector : 'a', remove : 'all', split : true, deep : true, 14907 onmatch : function(node) { 14908 return true; 14909 }, 14910 14911 onformat : function(elm, fmt, vars) { 14912 each(vars, function(value, key) { 14913 dom.setAttrib(elm, key, value); 14914 }); 14915 } 14916 }, 14917 14918 removeformat : [ 14919 {selector : 'b,strong,em,i,font,u,strike', remove : 'all', split : true, expand : false, block_expand : true, deep : true}, 14920 {selector : 'span', attributes : ['style', 'class'], remove : 'empty', split : true, expand : false, deep : true}, 14921 {selector : '*', attributes : ['style', 'class'], split : false, expand : false, deep : true} 14922 ] 14923 }); 14924 14925 // Register default block formats 14926 each('p h1 h2 h3 h4 h5 h6 div address pre div code dt dd samp'.split(/\s/), function(name) { 14927 register(name, {block : name, remove : 'all'}); 14928 }); 14929 14930 // Register user defined formats 14931 register(ed.settings.formats); 14932 }; 14933 14934 function addKeyboardShortcuts() { 14935 // Add some inline shortcuts 14936 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 14937 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 14938 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 14939 14940 // BlockFormat shortcuts keys 14941 for (var i = 1; i <= 6; i++) { 14942 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 14943 } 14944 14945 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 14946 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 14947 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 14948 }; 14949 14950 // Public functions 14951 14952 function get(name) { 14953 return name ? formats[name] : formats; 14954 }; 14955 14956 function register(name, format) { 14957 if (name) { 14958 if (typeof(name) !== 'string') { 14959 each(name, function(format, name) { 14960 register(name, format); 14961 }); 14962 } else { 14963 // Force format into array and add it to internal collection 14964 format = format.length ? format : [format]; 14965 14966 each(format, function(format) { 14967 // Set deep to false by default on selector formats this to avoid removing 14968 // alignment on images inside paragraphs when alignment is changed on paragraphs 14969 if (format.deep === undef) 14970 format.deep = !format.selector; 14971 14972 // Default to true 14973 if (format.split === undef) 14974 format.split = !format.selector || format.inline; 14975 14976 // Default to true 14977 if (format.remove === undef && format.selector && !format.inline) 14978 format.remove = 'none'; 14979 14980 // Mark format as a mixed format inline + block level 14981 if (format.selector && format.inline) { 14982 format.mixed = true; 14983 format.block_expand = true; 14984 } 14985 14986 // Split classes if needed 14987 if (typeof(format.classes) === 'string') 14988 format.classes = format.classes.split(/\s+/); 14989 }); 14990 14991 formats[name] = format; 14992 } 14993 } 14994 }; 14995 14996 var getTextDecoration = function(node) { 14997 var decoration; 14998 14999 ed.dom.getParent(node, function(n) { 15000 decoration = ed.dom.getStyle(n, 'text-decoration'); 15001 return decoration && decoration !== 'none'; 15002 }); 15003 15004 return decoration; 15005 }; 15006 15007 var processUnderlineAndColor = function(node) { 15008 var textDecoration; 15009 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 15010 textDecoration = getTextDecoration(node.parentNode); 15011 if (ed.dom.getStyle(node, 'color') && textDecoration) { 15012 ed.dom.setStyle(node, 'text-decoration', textDecoration); 15013 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 15014 ed.dom.setStyle(node, 'text-decoration', null); 15015 } 15016 } 15017 }; 15018 15019 function apply(name, vars, node) { 15020 var formatList = get(name), format = formatList[0], bookmark, rng, i, isCollapsed = selection.isCollapsed(); 15021 15022 function setElementFormat(elm, fmt) { 15023 fmt = fmt || format; 15024 15025 if (elm) { 15026 if (fmt.onformat) { 15027 fmt.onformat(elm, fmt, vars, node); 15028 } 15029 15030 each(fmt.styles, function(value, name) { 15031 dom.setStyle(elm, name, replaceVars(value, vars)); 15032 }); 15033 15034 each(fmt.attributes, function(value, name) { 15035 dom.setAttrib(elm, name, replaceVars(value, vars)); 15036 }); 15037 15038 each(fmt.classes, function(value) { 15039 value = replaceVars(value, vars); 15040 15041 if (!dom.hasClass(elm, value)) 15042 dom.addClass(elm, value); 15043 }); 15044 } 15045 }; 15046 function adjustSelectionToVisibleSelection() { 15047 function findSelectionEnd(start, end) { 15048 var walker = new TreeWalker(end); 15049 for (node = walker.current(); node; node = walker.prev()) { 15050 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 15051 return node; 15052 } 15053 } 15054 }; 15055 15056 // Adjust selection so that a end container with a end offset of zero is not included in the selection 15057 // as this isn't visible to the user. 15058 var rng = ed.selection.getRng(); 15059 var start = rng.startContainer; 15060 var end = rng.endContainer; 15061 15062 if (start != end && rng.endOffset === 0) { 15063 var newEnd = findSelectionEnd(start, end); 15064 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 15065 15066 rng.setEnd(newEnd, endOffset); 15067 } 15068 15069 return rng; 15070 } 15071 15072 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ 15073 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; 15074 15075 // find the index of the first child list. 15076 each(node.childNodes, function(n, index) { 15077 if (n.nodeName === "UL" || n.nodeName === "OL") { 15078 listIndex = index; 15079 list = n; 15080 return false; 15081 } 15082 }); 15083 15084 // get the index of the bookmarks 15085 each(node.childNodes, function(n, index) { 15086 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { 15087 if (n.id == bookmark.id + "_start") { 15088 startIndex = index; 15089 } else if (n.id == bookmark.id + "_end") { 15090 endIndex = index; 15091 } 15092 } 15093 }); 15094 15095 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally 15096 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { 15097 each(tinymce.grep(node.childNodes), process); 15098 return 0; 15099 } else { 15100 currentWrapElm = dom.clone(wrapElm, FALSE); 15101 15102 // create a list of the nodes on the same side of the list as the selection 15103 each(tinymce.grep(node.childNodes), function(n, index) { 15104 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { 15105 nodes.push(n); 15106 n.parentNode.removeChild(n); 15107 } 15108 }); 15109 15110 // insert the wrapping element either before or after the list. 15111 if (startIndex < listIndex) { 15112 node.insertBefore(currentWrapElm, list); 15113 } else if (startIndex > listIndex) { 15114 node.insertBefore(currentWrapElm, list.nextSibling); 15115 } 15116 15117 // add the new nodes to the list. 15118 newWrappers.push(currentWrapElm); 15119 15120 each(nodes, function(node) { 15121 currentWrapElm.appendChild(node); 15122 }); 15123 15124 return currentWrapElm; 15125 } 15126 }; 15127 15128 function applyRngStyle(rng, bookmark, node_specific) { 15129 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 15130 15131 // Setup wrapper element 15132 wrapName = format.inline || format.block; 15133 wrapElm = dom.create(wrapName); 15134 setElementFormat(wrapElm); 15135 15136 rangeUtils.walk(rng, function(nodes) { 15137 var currentWrapElm; 15138 15139 function process(node) { 15140 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 15141 15142 lastContentEditable = contentEditable; 15143 nodeName = node.nodeName.toLowerCase(); 15144 parentName = node.parentNode.nodeName.toLowerCase(); 15145 15146 // Node has a contentEditable value 15147 if (node.nodeType === 1 && getContentEditable(node)) { 15148 lastContentEditable = contentEditable; 15149 contentEditable = getContentEditable(node) === "true"; 15150 hasContentEditableState = true; // We don't want to wrap the container only it's children 15151 } 15152 15153 // Stop wrapping on br elements 15154 if (isEq(nodeName, 'br')) { 15155 currentWrapElm = 0; 15156 15157 // Remove any br elements when we wrap things 15158 if (format.block) 15159 dom.remove(node); 15160 15161 return; 15162 } 15163 15164 // If node is wrapper type 15165 if (format.wrapper && matchNode(node, name, vars)) { 15166 currentWrapElm = 0; 15167 return; 15168 } 15169 15170 // Can we rename the block 15171 if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName)) { 15172 node = dom.rename(node, wrapName); 15173 setElementFormat(node); 15174 newWrappers.push(node); 15175 currentWrapElm = 0; 15176 return; 15177 } 15178 15179 // Handle selector patterns 15180 if (format.selector) { 15181 // Look for matching formats 15182 each(formatList, function(format) { 15183 // Check collapsed state if it exists 15184 if ('collapsed' in format && format.collapsed !== isCollapsed) { 15185 return; 15186 } 15187 15188 if (dom.is(node, format.selector) && !isCaretNode(node)) { 15189 setElementFormat(node, format); 15190 found = true; 15191 } 15192 }); 15193 15194 // Continue processing if a selector match wasn't found and a inline element is defined 15195 if (!format.inline || found) { 15196 currentWrapElm = 0; 15197 return; 15198 } 15199 } 15200 15201 // Is it valid to wrap this item 15202 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 15203 !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node)) { 15204 // Start wrapping 15205 if (!currentWrapElm) { 15206 // Wrap the node 15207 currentWrapElm = dom.clone(wrapElm, FALSE); 15208 node.parentNode.insertBefore(currentWrapElm, node); 15209 newWrappers.push(currentWrapElm); 15210 } 15211 15212 currentWrapElm.appendChild(node); 15213 } else if (nodeName == 'li' && bookmark) { 15214 // Start wrapping - if we are in a list node and have a bookmark, then we will always begin by wrapping in a new element. 15215 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); 15216 } else { 15217 // Start a new wrapper for possible children 15218 currentWrapElm = 0; 15219 15220 each(tinymce.grep(node.childNodes), process); 15221 15222 if (hasContentEditableState) { 15223 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15224 } 15225 15226 // End the last wrapper 15227 currentWrapElm = 0; 15228 } 15229 }; 15230 15231 // Process siblings from range 15232 each(nodes, process); 15233 }); 15234 15235 // Wrap links inside as well, for example color inside a link when the wrapper is around the link 15236 if (format.wrap_links === false) { 15237 each(newWrappers, function(node) { 15238 function process(node) { 15239 var i, currentWrapElm, children; 15240 15241 if (node.nodeName === 'A') { 15242 currentWrapElm = dom.clone(wrapElm, FALSE); 15243 newWrappers.push(currentWrapElm); 15244 15245 children = tinymce.grep(node.childNodes); 15246 for (i = 0; i < children.length; i++) 15247 currentWrapElm.appendChild(children[i]); 15248 15249 node.appendChild(currentWrapElm); 15250 } 15251 15252 each(tinymce.grep(node.childNodes), process); 15253 }; 15254 15255 process(node); 15256 }); 15257 } 15258 15259 // Cleanup 15260 15261 each(newWrappers, function(node) { 15262 var childCount; 15263 15264 function getChildCount(node) { 15265 var count = 0; 15266 15267 each(node.childNodes, function(node) { 15268 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) 15269 count++; 15270 }); 15271 15272 return count; 15273 }; 15274 15275 function mergeStyles(node) { 15276 var child, clone; 15277 15278 each(node.childNodes, function(node) { 15279 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 15280 child = node; 15281 return FALSE; // break loop 15282 } 15283 }); 15284 15285 // If child was found and of the same type as the current node 15286 if (child && matchName(child, format)) { 15287 clone = dom.clone(child, FALSE); 15288 setElementFormat(clone); 15289 15290 dom.replace(clone, node, TRUE); 15291 dom.remove(child, 1); 15292 } 15293 15294 return clone || node; 15295 }; 15296 15297 childCount = getChildCount(node); 15298 15299 // Remove empty nodes but only if there is multiple wrappers and they are not block 15300 // elements so never remove single <h1></h1> since that would remove the currrent empty block element where the caret is at 15301 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 15302 dom.remove(node, 1); 15303 return; 15304 } 15305 15306 if (format.inline || format.wrapper) { 15307 // Merges the current node with it's children of similar type to reduce the number of elements 15308 if (!format.exact && childCount === 1) 15309 node = mergeStyles(node); 15310 15311 // Remove/merge children 15312 each(formatList, function(format) { 15313 // Merge all children of similar type will move styles from child to parent 15314 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 15315 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 15316 each(dom.select(format.inline, node), function(child) { 15317 var parent; 15318 15319 // When wrap_links is set to false we don't want 15320 // to remove the format on children within links 15321 if (format.wrap_links === false) { 15322 parent = child.parentNode; 15323 15324 do { 15325 if (parent.nodeName === 'A') 15326 return; 15327 } while (parent = parent.parentNode); 15328 } 15329 15330 removeFormat(format, vars, child, format.exact ? child : null); 15331 }); 15332 }); 15333 15334 // Remove child if direct parent is of same type 15335 if (matchNode(node.parentNode, name, vars)) { 15336 dom.remove(node, 1); 15337 node = 0; 15338 return TRUE; 15339 } 15340 15341 // Look for parent with similar style format 15342 if (format.merge_with_parents) { 15343 dom.getParent(node.parentNode, function(parent) { 15344 if (matchNode(parent, name, vars)) { 15345 dom.remove(node, 1); 15346 node = 0; 15347 return TRUE; 15348 } 15349 }); 15350 } 15351 15352 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 15353 if (node && format.merge_siblings !== false) { 15354 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 15355 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 15356 } 15357 } 15358 }); 15359 }; 15360 15361 if (format) { 15362 if (node) { 15363 if (node.nodeType) { 15364 rng = dom.createRng(); 15365 rng.setStartBefore(node); 15366 rng.setEndAfter(node); 15367 applyRngStyle(expandRng(rng, formatList), null, true); 15368 } else { 15369 applyRngStyle(node, null, true); 15370 } 15371 } else { 15372 if (!isCollapsed || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15373 // Obtain selection node before selection is unselected by applyRngStyle() 15374 var curSelNode = ed.selection.getNode(); 15375 15376 // If the formats have a default block and we can't find a parent block then start wrapping it with a DIV this is for forced_root_blocks: false 15377 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 15378 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 15379 apply(formatList[0].defaultBlock); 15380 } 15381 15382 // Apply formatting to selection 15383 ed.selection.setRng(adjustSelectionToVisibleSelection()); 15384 bookmark = selection.getBookmark(); 15385 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 15386 15387 // Colored nodes should be underlined so that the color of the underline matches the text color. 15388 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 15389 tinymce.walk(curSelNode, processUnderlineAndColor, 'childNodes'); 15390 processUnderlineAndColor(curSelNode); 15391 } 15392 15393 selection.moveToBookmark(bookmark); 15394 moveStart(selection.getRng(TRUE)); 15395 ed.nodeChanged(); 15396 } else 15397 performCaretAction('apply', name, vars); 15398 } 15399 } 15400 }; 15401 15402 function remove(name, vars, node) { 15403 var formatList = get(name), format = formatList[0], bookmark, i, rng, contentEditable = true; 15404 15405 // Merges the styles for each node 15406 function process(node) { 15407 var children, i, l, localContentEditable, lastContentEditable, hasContentEditableState; 15408 15409 // Node has a contentEditable value 15410 if (node.nodeType === 1 && getContentEditable(node)) { 15411 lastContentEditable = contentEditable; 15412 contentEditable = getContentEditable(node) === "true"; 15413 hasContentEditableState = true; // We don't want to wrap the container only it's children 15414 } 15415 15416 // Grab the children first since the nodelist might be changed 15417 children = tinymce.grep(node.childNodes); 15418 15419 // Process current node 15420 if (contentEditable && !hasContentEditableState) { 15421 for (i = 0, l = formatList.length; i < l; i++) { 15422 if (removeFormat(formatList[i], vars, node, node)) 15423 break; 15424 } 15425 } 15426 15427 // Process the children 15428 if (format.deep) { 15429 if (children.length) { 15430 for (i = 0, l = children.length; i < l; i++) 15431 process(children[i]); 15432 15433 if (hasContentEditableState) { 15434 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15435 } 15436 } 15437 } 15438 }; 15439 15440 function findFormatRoot(container) { 15441 var formatRoot; 15442 15443 // Find format root 15444 each(getParents(container.parentNode).reverse(), function(parent) { 15445 var format; 15446 15447 // Find format root element 15448 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 15449 // Is the node matching the format we are looking for 15450 format = matchNode(parent, name, vars); 15451 if (format && format.split !== false) 15452 formatRoot = parent; 15453 } 15454 }); 15455 15456 return formatRoot; 15457 }; 15458 15459 function wrapAndSplit(format_root, container, target, split) { 15460 var parent, clone, lastClone, firstClone, i, formatRootParent; 15461 15462 // Format root found then clone formats and split it 15463 if (format_root) { 15464 formatRootParent = format_root.parentNode; 15465 15466 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 15467 clone = dom.clone(parent, FALSE); 15468 15469 for (i = 0; i < formatList.length; i++) { 15470 if (removeFormat(formatList[i], vars, clone, clone)) { 15471 clone = 0; 15472 break; 15473 } 15474 } 15475 15476 // Build wrapper node 15477 if (clone) { 15478 if (lastClone) 15479 clone.appendChild(lastClone); 15480 15481 if (!firstClone) 15482 firstClone = clone; 15483 15484 lastClone = clone; 15485 } 15486 } 15487 15488 // Never split block elements if the format is mixed 15489 if (split && (!format.mixed || !isBlock(format_root))) 15490 container = dom.split(format_root, container); 15491 15492 // Wrap container in cloned formats 15493 if (lastClone) { 15494 target.parentNode.insertBefore(lastClone, target); 15495 firstClone.appendChild(target); 15496 } 15497 } 15498 15499 return container; 15500 }; 15501 15502 function splitToFormatRoot(container) { 15503 return wrapAndSplit(findFormatRoot(container), container, container, true); 15504 }; 15505 15506 function unwrap(start) { 15507 var node = dom.get(start ? '_start' : '_end'), 15508 out = node[start ? 'firstChild' : 'lastChild']; 15509 15510 // If the end is placed within the start the result will be removed 15511 // So this checks if the out node is a bookmark node if it is it 15512 // checks for another more suitable node 15513 if (isBookmarkNode(out)) 15514 out = out[start ? 'firstChild' : 'lastChild']; 15515 15516 dom.remove(node, true); 15517 15518 return out; 15519 }; 15520 15521 function removeRngStyle(rng) { 15522 var startContainer, endContainer, node; 15523 15524 rng = expandRng(rng, formatList, TRUE); 15525 15526 if (format.split) { 15527 startContainer = getContainer(rng, TRUE); 15528 endContainer = getContainer(rng); 15529 15530 if (startContainer != endContainer) { 15531 // WebKit will render the table incorrectly if we wrap a TD in a SPAN so lets see if the can use the first child instead 15532 // This will happen if you tripple click a table cell and use remove formatting 15533 if (/^(TR|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 15534 startContainer = (startContainer.nodeName == "TD" ? startContainer.firstChild : startContainer.firstChild.firstChild) || startContainer; 15535 } 15536 15537 // Wrap start/end nodes in span element since these might be cloned/moved 15538 startContainer = wrap(startContainer, 'span', {id : '_start', 'data-mce-type' : 'bookmark'}); 15539 endContainer = wrap(endContainer, 'span', {id : '_end', 'data-mce-type' : 'bookmark'}); 15540 15541 // Split start/end 15542 splitToFormatRoot(startContainer); 15543 splitToFormatRoot(endContainer); 15544 15545 // Unwrap start/end to get real elements again 15546 startContainer = unwrap(TRUE); 15547 endContainer = unwrap(); 15548 } else 15549 startContainer = endContainer = splitToFormatRoot(startContainer); 15550 15551 // Update range positions since they might have changed after the split operations 15552 rng.startContainer = startContainer.parentNode; 15553 rng.startOffset = nodeIndex(startContainer); 15554 rng.endContainer = endContainer.parentNode; 15555 rng.endOffset = nodeIndex(endContainer) + 1; 15556 } 15557 15558 // Remove items between start/end 15559 rangeUtils.walk(rng, function(nodes) { 15560 each(nodes, function(node) { 15561 process(node); 15562 15563 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 15564 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 15565 removeFormat({'deep': false, 'exact': true, 'inline': 'span', 'styles': {'textDecoration' : 'underline'}}, null, node); 15566 } 15567 }); 15568 }); 15569 }; 15570 15571 // Handle node 15572 if (node) { 15573 if (node.nodeType) { 15574 rng = dom.createRng(); 15575 rng.setStartBefore(node); 15576 rng.setEndAfter(node); 15577 removeRngStyle(rng); 15578 } else { 15579 removeRngStyle(node); 15580 } 15581 15582 return; 15583 } 15584 15585 if (!selection.isCollapsed() || !format.inline || dom.select('td.mceSelected,th.mceSelected').length) { 15586 bookmark = selection.getBookmark(); 15587 removeRngStyle(selection.getRng(TRUE)); 15588 selection.moveToBookmark(bookmark); 15589 15590 // Check if start element still has formatting then we are at: "<b>text|</b>text" and need to move the start into the next text node 15591 if (format.inline && match(name, vars, selection.getStart())) { 15592 moveStart(selection.getRng(true)); 15593 } 15594 15595 ed.nodeChanged(); 15596 } else 15597 performCaretAction('remove', name, vars); 15598 }; 15599 15600 function toggle(name, vars, node) { 15601 var fmt = get(name); 15602 15603 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) 15604 remove(name, vars, node); 15605 else 15606 apply(name, vars, node); 15607 }; 15608 15609 function matchNode(node, name, vars, similar) { 15610 var formatList = get(name), format, i, classes; 15611 15612 function matchItems(node, format, item_name) { 15613 var key, value, items = format[item_name], i; 15614 15615 // Custom match 15616 if (format.onmatch) { 15617 return format.onmatch(node, format, item_name); 15618 } 15619 15620 // Check all items 15621 if (items) { 15622 // Non indexed object 15623 if (items.length === undef) { 15624 for (key in items) { 15625 if (items.hasOwnProperty(key)) { 15626 if (item_name === 'attributes') 15627 value = dom.getAttrib(node, key); 15628 else 15629 value = getStyle(node, key); 15630 15631 if (similar && !value && !format.exact) 15632 return; 15633 15634 if ((!similar || format.exact) && !isEq(value, replaceVars(items[key], vars))) 15635 return; 15636 } 15637 } 15638 } else { 15639 // Only one match needed for indexed arrays 15640 for (i = 0; i < items.length; i++) { 15641 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) 15642 return format; 15643 } 15644 } 15645 } 15646 15647 return format; 15648 }; 15649 15650 if (formatList && node) { 15651 // Check each format in list 15652 for (i = 0; i < formatList.length; i++) { 15653 format = formatList[i]; 15654 15655 // Name name, attributes, styles and classes 15656 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 15657 // Match classes 15658 if (classes = format.classes) { 15659 for (i = 0; i < classes.length; i++) { 15660 if (!dom.hasClass(node, classes[i])) 15661 return; 15662 } 15663 } 15664 15665 return format; 15666 } 15667 } 15668 } 15669 }; 15670 15671 function match(name, vars, node) { 15672 var startNode; 15673 15674 function matchParents(node) { 15675 // Find first node with similar format settings 15676 node = dom.getParent(node, function(node) { 15677 return !!matchNode(node, name, vars, true); 15678 }); 15679 15680 // Do an exact check on the similar format element 15681 return matchNode(node, name, vars); 15682 }; 15683 15684 // Check specified node 15685 if (node) 15686 return matchParents(node); 15687 15688 // Check selected node 15689 node = selection.getNode(); 15690 if (matchParents(node)) 15691 return TRUE; 15692 15693 // Check start node if it's different 15694 startNode = selection.getStart(); 15695 if (startNode != node) { 15696 if (matchParents(startNode)) 15697 return TRUE; 15698 } 15699 15700 return FALSE; 15701 }; 15702 15703 function matchAll(names, vars) { 15704 var startElement, matchedFormatNames = [], checkedMap = {}, i, ni, name; 15705 15706 // Check start of selection for formats 15707 startElement = selection.getStart(); 15708 dom.getParent(startElement, function(node) { 15709 var i, name; 15710 15711 for (i = 0; i < names.length; i++) { 15712 name = names[i]; 15713 15714 if (!checkedMap[name] && matchNode(node, name, vars)) { 15715 checkedMap[name] = true; 15716 matchedFormatNames.push(name); 15717 } 15718 } 15719 }, dom.getRoot()); 15720 15721 return matchedFormatNames; 15722 }; 15723 15724 function canApply(name) { 15725 var formatList = get(name), startNode, parents, i, x, selector; 15726 15727 if (formatList) { 15728 startNode = selection.getStart(); 15729 parents = getParents(startNode); 15730 15731 for (x = formatList.length - 1; x >= 0; x--) { 15732 selector = formatList[x].selector; 15733 15734 // Format is not selector based, then always return TRUE 15735 if (!selector) 15736 return TRUE; 15737 15738 for (i = parents.length - 1; i >= 0; i--) { 15739 if (dom.is(parents[i], selector)) 15740 return TRUE; 15741 } 15742 } 15743 } 15744 15745 return FALSE; 15746 }; 15747 15748 function formatChanged(formats, callback) { 15749 var currentFormats; 15750 15751 // Setup format node change logic 15752 if (!formatChangeData) { 15753 formatChangeData = {}; 15754 currentFormats = {}; 15755 15756 ed.onNodeChange.addToTop(function(ed, cm, node) { 15757 var parents = getParents(node), matchedFormats = {}; 15758 15759 // Check for new formats 15760 each(formatChangeData, function(callbacks, format) { 15761 each(parents, function(node) { 15762 if (matchNode(node, format, {}, true)) { 15763 if (!currentFormats[format]) { 15764 // Execute callbacks 15765 each(callbacks, function(callback) { 15766 callback(true, {node: node, format: format, parents: parents}); 15767 }); 15768 15769 currentFormats[format] = callbacks; 15770 } 15771 15772 matchedFormats[format] = callbacks; 15773 return false; 15774 } 15775 }); 15776 }); 15777 15778 // Check if current formats still match 15779 each(currentFormats, function(callbacks, format) { 15780 if (!matchedFormats[format]) { 15781 delete currentFormats[format]; 15782 15783 each(callbacks, function(callback) { 15784 callback(false, {node: node, format: format, parents: parents}); 15785 }); 15786 } 15787 }); 15788 }); 15789 } 15790 15791 // Add format listeners 15792 each(formats.split(','), function(format) { 15793 if (!formatChangeData[format]) { 15794 formatChangeData[format] = []; 15795 } 15796 15797 formatChangeData[format].push(callback); 15798 }); 15799 15800 return this; 15801 }; 15802 15803 // Expose to public 15804 tinymce.extend(this, { 15805 get : get, 15806 register : register, 15807 apply : apply, 15808 remove : remove, 15809 toggle : toggle, 15810 match : match, 15811 matchAll : matchAll, 15812 matchNode : matchNode, 15813 canApply : canApply, 15814 formatChanged: formatChanged 15815 }); 15816 15817 // Initialize 15818 defaultFormats(); 15819 addKeyboardShortcuts(); 15820 15821 // Private functions 15822 15823 function matchName(node, format) { 15824 // Check for inline match 15825 if (isEq(node, format.inline)) 15826 return TRUE; 15827 15828 // Check for block match 15829 if (isEq(node, format.block)) 15830 return TRUE; 15831 15832 // Check for selector match 15833 if (format.selector) 15834 return dom.is(node, format.selector); 15835 }; 15836 15837 function isEq(str1, str2) { 15838 str1 = str1 || ''; 15839 str2 = str2 || ''; 15840 15841 str1 = '' + (str1.nodeName || str1); 15842 str2 = '' + (str2.nodeName || str2); 15843 15844 return str1.toLowerCase() == str2.toLowerCase(); 15845 }; 15846 15847 function getStyle(node, name) { 15848 var styleVal = dom.getStyle(node, name); 15849 15850 // Force the format to hex 15851 if (name == 'color' || name == 'backgroundColor') 15852 styleVal = dom.toHex(styleVal); 15853 15854 // Opera will return bold as 700 15855 if (name == 'fontWeight' && styleVal == 700) 15856 styleVal = 'bold'; 15857 15858 return '' + styleVal; 15859 }; 15860 15861 function replaceVars(value, vars) { 15862 if (typeof(value) != "string") 15863 value = value(vars); 15864 else if (vars) { 15865 value = value.replace(/%(\w+)/g, function(str, name) { 15866 return vars[name] || str; 15867 }); 15868 } 15869 15870 return value; 15871 }; 15872 15873 function isWhiteSpaceNode(node) { 15874 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 15875 }; 15876 15877 function wrap(node, name, attrs) { 15878 var wrapper = dom.create(name, attrs); 15879 15880 node.parentNode.insertBefore(wrapper, node); 15881 wrapper.appendChild(node); 15882 15883 return wrapper; 15884 }; 15885 15886 function expandRng(rng, format, remove) { 15887 var sibling, lastIdx, leaf, endPoint, 15888 startContainer = rng.startContainer, 15889 startOffset = rng.startOffset, 15890 endContainer = rng.endContainer, 15891 endOffset = rng.endOffset; 15892 15893 // This function walks up the tree if there is no siblings before/after the node 15894 function findParentContainer(start) { 15895 var container, parent, child, sibling, siblingName, root; 15896 15897 container = parent = start ? startContainer : endContainer; 15898 siblingName = start ? 'previousSibling' : 'nextSibling'; 15899 root = dom.getRoot(); 15900 15901 // If it's a text node and the offset is inside the text 15902 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 15903 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 15904 return container; 15905 } 15906 } 15907 15908 for (;;) { 15909 // Stop expanding on block elements 15910 if (!format[0].block_expand && isBlock(parent)) 15911 return parent; 15912 15913 // Walk left/right 15914 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 15915 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling)) { 15916 return parent; 15917 } 15918 } 15919 15920 // Check if we can move up are we at root level or body level 15921 if (parent.parentNode == root) { 15922 container = parent; 15923 break; 15924 } 15925 15926 parent = parent.parentNode; 15927 } 15928 15929 return container; 15930 }; 15931 15932 // This function walks down the tree to find the leaf at the selection. 15933 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 15934 function findLeaf(node, offset) { 15935 if (offset === undef) 15936 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15937 while (node && node.hasChildNodes()) { 15938 node = node.childNodes[offset]; 15939 if (node) 15940 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 15941 } 15942 return { node: node, offset: offset }; 15943 } 15944 15945 // If index based start position then resolve it 15946 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 15947 lastIdx = startContainer.childNodes.length - 1; 15948 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 15949 15950 if (startContainer.nodeType == 3) 15951 startOffset = 0; 15952 } 15953 15954 // If index based end position then resolve it 15955 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 15956 lastIdx = endContainer.childNodes.length - 1; 15957 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 15958 15959 if (endContainer.nodeType == 3) 15960 endOffset = endContainer.nodeValue.length; 15961 } 15962 15963 // Expands the node to the closes contentEditable false element if it exists 15964 function findParentContentEditable(node) { 15965 var parent = node; 15966 15967 while (parent) { 15968 if (parent.nodeType === 1 && getContentEditable(parent)) { 15969 return getContentEditable(parent) === "false" ? parent : node; 15970 } 15971 15972 parent = parent.parentNode; 15973 } 15974 15975 return node; 15976 }; 15977 15978 function findWordEndPoint(container, offset, start) { 15979 var walker, node, pos, lastTextNode; 15980 15981 function findSpace(node, offset) { 15982 var pos, pos2, str = node.nodeValue; 15983 15984 if (typeof(offset) == "undefined") { 15985 offset = start ? str.length : 0; 15986 } 15987 15988 if (start) { 15989 pos = str.lastIndexOf(' ', offset); 15990 pos2 = str.lastIndexOf('\u00a0', offset); 15991 pos = pos > pos2 ? pos : pos2; 15992 15993 // Include the space on remove to avoid tag soup 15994 if (pos !== -1 && !remove) { 15995 pos++; 15996 } 15997 } else { 15998 pos = str.indexOf(' ', offset); 15999 pos2 = str.indexOf('\u00a0', offset); 16000 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 16001 } 16002 16003 return pos; 16004 }; 16005 16006 if (container.nodeType === 3) { 16007 pos = findSpace(container, offset); 16008 16009 if (pos !== -1) { 16010 return {container : container, offset : pos}; 16011 } 16012 16013 lastTextNode = container; 16014 } 16015 16016 // Walk the nodes inside the block 16017 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 16018 while (node = walker[start ? 'prev' : 'next']()) { 16019 if (node.nodeType === 3) { 16020 lastTextNode = node; 16021 pos = findSpace(node); 16022 16023 if (pos !== -1) { 16024 return {container : node, offset : pos}; 16025 } 16026 } else if (isBlock(node)) { 16027 break; 16028 } 16029 } 16030 16031 if (lastTextNode) { 16032 if (start) { 16033 offset = 0; 16034 } else { 16035 offset = lastTextNode.length; 16036 } 16037 16038 return {container: lastTextNode, offset: offset}; 16039 } 16040 }; 16041 16042 function findSelectorEndPoint(container, sibling_name) { 16043 var parents, i, y, curFormat; 16044 16045 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) 16046 container = container[sibling_name]; 16047 16048 parents = getParents(container); 16049 for (i = 0; i < parents.length; i++) { 16050 for (y = 0; y < format.length; y++) { 16051 curFormat = format[y]; 16052 16053 // If collapsed state is set then skip formats that doesn't match that 16054 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) 16055 continue; 16056 16057 if (dom.is(parents[i], curFormat.selector)) 16058 return parents[i]; 16059 } 16060 } 16061 16062 return container; 16063 }; 16064 16065 function findBlockEndPoint(container, sibling_name, sibling_name2) { 16066 var node; 16067 16068 // Expand to block of similar type 16069 if (!format[0].wrapper) 16070 node = dom.getParent(container, format[0].block); 16071 16072 // Expand to first wrappable block element or any block element 16073 if (!node) 16074 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, isBlock); 16075 16076 // Exclude inner lists from wrapping 16077 if (node && format[0].wrapper) 16078 node = getParents(node, 'ul,ol').reverse()[0] || node; 16079 16080 // Didn't find a block element look for first/last wrappable element 16081 if (!node) { 16082 node = container; 16083 16084 while (node[sibling_name] && !isBlock(node[sibling_name])) { 16085 node = node[sibling_name]; 16086 16087 // Break on BR but include it will be removed later on 16088 // we can't remove it now since we need to check if it can be wrapped 16089 if (isEq(node, 'br')) 16090 break; 16091 } 16092 } 16093 16094 return node || container; 16095 }; 16096 16097 // Expand to closest contentEditable element 16098 startContainer = findParentContentEditable(startContainer); 16099 endContainer = findParentContentEditable(endContainer); 16100 16101 // Exclude bookmark nodes if possible 16102 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 16103 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 16104 startContainer = startContainer.nextSibling || startContainer; 16105 16106 if (startContainer.nodeType == 3) 16107 startOffset = 0; 16108 } 16109 16110 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 16111 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 16112 endContainer = endContainer.previousSibling || endContainer; 16113 16114 if (endContainer.nodeType == 3) 16115 endOffset = endContainer.length; 16116 } 16117 16118 if (format[0].inline) { 16119 if (rng.collapsed) { 16120 // Expand left to closest word boundery 16121 endPoint = findWordEndPoint(startContainer, startOffset, true); 16122 if (endPoint) { 16123 startContainer = endPoint.container; 16124 startOffset = endPoint.offset; 16125 } 16126 16127 // Expand right to closest word boundery 16128 endPoint = findWordEndPoint(endContainer, endOffset); 16129 if (endPoint) { 16130 endContainer = endPoint.container; 16131 endOffset = endPoint.offset; 16132 } 16133 } 16134 16135 // Avoid applying formatting to a trailing space. 16136 leaf = findLeaf(endContainer, endOffset); 16137 if (leaf.node) { 16138 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) 16139 leaf = findLeaf(leaf.node.previousSibling); 16140 16141 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 16142 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 16143 16144 if (leaf.offset > 1) { 16145 endContainer = leaf.node; 16146 endContainer.splitText(leaf.offset - 1); 16147 } 16148 } 16149 } 16150 } 16151 16152 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 16153 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 16154 // This will reduce the number of wrapper elements that needs to be created 16155 // Move start point up the tree 16156 if (format[0].inline || format[0].block_expand) { 16157 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 16158 startContainer = findParentContainer(true); 16159 } 16160 16161 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 16162 endContainer = findParentContainer(); 16163 } 16164 } 16165 16166 // Expand start/end container to matching selector 16167 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 16168 // Find new startContainer/endContainer if there is better one 16169 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 16170 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 16171 } 16172 16173 // Expand start/end container to matching block element or text node 16174 if (format[0].block || format[0].selector) { 16175 // Find new startContainer/endContainer if there is better one 16176 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 16177 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 16178 16179 // Non block element then try to expand up the leaf 16180 if (format[0].block) { 16181 if (!isBlock(startContainer)) 16182 startContainer = findParentContainer(true); 16183 16184 if (!isBlock(endContainer)) 16185 endContainer = findParentContainer(); 16186 } 16187 } 16188 16189 // Setup index for startContainer 16190 if (startContainer.nodeType == 1) { 16191 startOffset = nodeIndex(startContainer); 16192 startContainer = startContainer.parentNode; 16193 } 16194 16195 // Setup index for endContainer 16196 if (endContainer.nodeType == 1) { 16197 endOffset = nodeIndex(endContainer) + 1; 16198 endContainer = endContainer.parentNode; 16199 } 16200 16201 // Return new range like object 16202 return { 16203 startContainer : startContainer, 16204 startOffset : startOffset, 16205 endContainer : endContainer, 16206 endOffset : endOffset 16207 }; 16208 } 16209 16210 function removeFormat(format, vars, node, compare_node) { 16211 var i, attrs, stylesModified; 16212 16213 // Check if node matches format 16214 if (!matchName(node, format)) 16215 return FALSE; 16216 16217 // Should we compare with format attribs and styles 16218 if (format.remove != 'all') { 16219 // Remove styles 16220 each(format.styles, function(value, name) { 16221 value = replaceVars(value, vars); 16222 16223 // Indexed array 16224 if (typeof(name) === 'number') { 16225 name = value; 16226 compare_node = 0; 16227 } 16228 16229 if (!compare_node || isEq(getStyle(compare_node, name), value)) 16230 dom.setStyle(node, name, ''); 16231 16232 stylesModified = 1; 16233 }); 16234 16235 // Remove style attribute if it's empty 16236 if (stylesModified && dom.getAttrib(node, 'style') == '') { 16237 node.removeAttribute('style'); 16238 node.removeAttribute('data-mce-style'); 16239 } 16240 16241 // Remove attributes 16242 each(format.attributes, function(value, name) { 16243 var valueOut; 16244 16245 value = replaceVars(value, vars); 16246 16247 // Indexed array 16248 if (typeof(name) === 'number') { 16249 name = value; 16250 compare_node = 0; 16251 } 16252 16253 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 16254 // Keep internal classes 16255 if (name == 'class') { 16256 value = dom.getAttrib(node, name); 16257 if (value) { 16258 // Build new class value where everything is removed except the internal prefixed classes 16259 valueOut = ''; 16260 each(value.split(/\s+/), function(cls) { 16261 if (/mce\w+/.test(cls)) 16262 valueOut += (valueOut ? ' ' : '') + cls; 16263 }); 16264 16265 // We got some internal classes left 16266 if (valueOut) { 16267 dom.setAttrib(node, name, valueOut); 16268 return; 16269 } 16270 } 16271 } 16272 16273 // IE6 has a bug where the attribute doesn't get removed correctly 16274 if (name == "class") 16275 node.removeAttribute('className'); 16276 16277 // Remove mce prefixed attributes 16278 if (MCE_ATTR_RE.test(name)) 16279 node.removeAttribute('data-mce-' + name); 16280 16281 node.removeAttribute(name); 16282 } 16283 }); 16284 16285 // Remove classes 16286 each(format.classes, function(value) { 16287 value = replaceVars(value, vars); 16288 16289 if (!compare_node || dom.hasClass(compare_node, value)) 16290 dom.removeClass(node, value); 16291 }); 16292 16293 // Check for non internal attributes 16294 attrs = dom.getAttribs(node); 16295 for (i = 0; i < attrs.length; i++) { 16296 if (attrs[i].nodeName.indexOf('_') !== 0) 16297 return FALSE; 16298 } 16299 } 16300 16301 // Remove the inline child if it's empty for example <b> or <span> 16302 if (format.remove != 'none') { 16303 removeNode(node, format); 16304 return TRUE; 16305 } 16306 }; 16307 16308 function removeNode(node, format) { 16309 var parentNode = node.parentNode, rootBlockElm; 16310 16311 function find(node, next, inc) { 16312 node = getNonWhiteSpaceSibling(node, next, inc); 16313 16314 return !node || (node.nodeName == 'BR' || isBlock(node)); 16315 }; 16316 16317 if (format.block) { 16318 if (!forcedRootBlock) { 16319 // Append BR elements if needed before we remove the block 16320 if (isBlock(node) && !isBlock(parentNode)) { 16321 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) 16322 node.insertBefore(dom.create('br'), node.firstChild); 16323 16324 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) 16325 node.appendChild(dom.create('br')); 16326 } 16327 } else { 16328 // Wrap the block in a forcedRootBlock if we are at the root of document 16329 if (parentNode == dom.getRoot()) { 16330 if (!format.list_block || !isEq(node, format.list_block)) { 16331 each(tinymce.grep(node.childNodes), function(node) { 16332 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 16333 if (!rootBlockElm) 16334 rootBlockElm = wrap(node, forcedRootBlock); 16335 else 16336 rootBlockElm.appendChild(node); 16337 } else 16338 rootBlockElm = 0; 16339 }); 16340 } 16341 } 16342 } 16343 } 16344 16345 // Never remove nodes that isn't the specified inline element if a selector is specified too 16346 if (format.selector && format.inline && !isEq(format.inline, node)) 16347 return; 16348 16349 dom.remove(node, 1); 16350 }; 16351 16352 function getNonWhiteSpaceSibling(node, next, inc) { 16353 if (node) { 16354 next = next ? 'nextSibling' : 'previousSibling'; 16355 16356 for (node = inc ? node : node[next]; node; node = node[next]) { 16357 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) 16358 return node; 16359 } 16360 } 16361 }; 16362 16363 function isBookmarkNode(node) { 16364 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; 16365 }; 16366 16367 function mergeSiblings(prev, next) { 16368 var marker, sibling, tmpSibling; 16369 16370 function compareElements(node1, node2) { 16371 // Not the same name 16372 if (node1.nodeName != node2.nodeName) 16373 return FALSE; 16374 16375 function getAttribs(node) { 16376 var attribs = {}; 16377 16378 each(dom.getAttribs(node), function(attr) { 16379 var name = attr.nodeName.toLowerCase(); 16380 16381 // Don't compare internal attributes or style 16382 if (name.indexOf('_') !== 0 && name !== 'style') 16383 attribs[name] = dom.getAttrib(node, name); 16384 }); 16385 16386 return attribs; 16387 }; 16388 16389 function compareObjects(obj1, obj2) { 16390 var value, name; 16391 16392 for (name in obj1) { 16393 // Obj1 has item obj2 doesn't have 16394 if (obj1.hasOwnProperty(name)) { 16395 value = obj2[name]; 16396 16397 // Obj2 doesn't have obj1 item 16398 if (value === undef) 16399 return FALSE; 16400 16401 // Obj2 item has a different value 16402 if (obj1[name] != value) 16403 return FALSE; 16404 16405 // Delete similar value 16406 delete obj2[name]; 16407 } 16408 } 16409 16410 // Check if obj 2 has something obj 1 doesn't have 16411 for (name in obj2) { 16412 // Obj2 has item obj1 doesn't have 16413 if (obj2.hasOwnProperty(name)) 16414 return FALSE; 16415 } 16416 16417 return TRUE; 16418 }; 16419 16420 // Attribs are not the same 16421 if (!compareObjects(getAttribs(node1), getAttribs(node2))) 16422 return FALSE; 16423 16424 // Styles are not the same 16425 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) 16426 return FALSE; 16427 16428 return TRUE; 16429 }; 16430 16431 function findElementSibling(node, sibling_name) { 16432 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 16433 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) 16434 return node; 16435 16436 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) 16437 return sibling; 16438 } 16439 16440 return node; 16441 }; 16442 16443 // Check if next/prev exists and that they are elements 16444 if (prev && next) { 16445 // If previous sibling is empty then jump over it 16446 prev = findElementSibling(prev, 'previousSibling'); 16447 next = findElementSibling(next, 'nextSibling'); 16448 16449 // Compare next and previous nodes 16450 if (compareElements(prev, next)) { 16451 // Append nodes between 16452 for (sibling = prev.nextSibling; sibling && sibling != next;) { 16453 tmpSibling = sibling; 16454 sibling = sibling.nextSibling; 16455 prev.appendChild(tmpSibling); 16456 } 16457 16458 // Remove next node 16459 dom.remove(next); 16460 16461 // Move children into prev node 16462 each(tinymce.grep(next.childNodes), function(node) { 16463 prev.appendChild(node); 16464 }); 16465 16466 return prev; 16467 } 16468 } 16469 16470 return next; 16471 }; 16472 16473 function isTextBlock(name) { 16474 return /^(h[1-6]|p|div|pre|address|dl|dt|dd)$/.test(name); 16475 }; 16476 16477 function getContainer(rng, start) { 16478 var container, offset, lastIdx, walker; 16479 16480 container = rng[start ? 'startContainer' : 'endContainer']; 16481 offset = rng[start ? 'startOffset' : 'endOffset']; 16482 16483 if (container.nodeType == 1) { 16484 lastIdx = container.childNodes.length - 1; 16485 16486 if (!start && offset) 16487 offset--; 16488 16489 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 16490 } 16491 16492 // If start text node is excluded then walk to the next node 16493 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 16494 container = new TreeWalker(container, ed.getBody()).next() || container; 16495 } 16496 16497 // If end text node is excluded then walk to the previous node 16498 if (container.nodeType === 3 && !start && offset === 0) { 16499 container = new TreeWalker(container, ed.getBody()).prev() || container; 16500 } 16501 16502 return container; 16503 }; 16504 16505 function performCaretAction(type, name, vars) { 16506 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 16507 16508 // Creates a caret container bogus element 16509 function createCaretContainer(fill) { 16510 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 16511 16512 if (fill) { 16513 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 16514 } 16515 16516 return caretContainer; 16517 }; 16518 16519 function isCaretContainerEmpty(node, nodes) { 16520 while (node) { 16521 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 16522 return false; 16523 } 16524 16525 // Collect nodes 16526 if (nodes && node.nodeType === 1) { 16527 nodes.push(node); 16528 } 16529 16530 node = node.firstChild; 16531 } 16532 16533 return true; 16534 }; 16535 16536 // Returns any parent caret container element 16537 function getParentCaretContainer(node) { 16538 while (node) { 16539 if (node.id === caretContainerId) { 16540 return node; 16541 } 16542 16543 node = node.parentNode; 16544 } 16545 }; 16546 16547 // Finds the first text node in the specified node 16548 function findFirstTextNode(node) { 16549 var walker; 16550 16551 if (node) { 16552 walker = new TreeWalker(node, node); 16553 16554 for (node = walker.current(); node; node = walker.next()) { 16555 if (node.nodeType === 3) { 16556 return node; 16557 } 16558 } 16559 } 16560 }; 16561 16562 // Removes the caret container for the specified node or all on the current document 16563 function removeCaretContainer(node, move_caret) { 16564 var child, rng; 16565 16566 if (!node) { 16567 node = getParentCaretContainer(selection.getStart()); 16568 16569 if (!node) { 16570 while (node = dom.get(caretContainerId)) { 16571 removeCaretContainer(node, false); 16572 } 16573 } 16574 } else { 16575 rng = selection.getRng(true); 16576 16577 if (isCaretContainerEmpty(node)) { 16578 if (move_caret !== false) { 16579 rng.setStartBefore(node); 16580 rng.setEndBefore(node); 16581 } 16582 16583 dom.remove(node); 16584 } else { 16585 child = findFirstTextNode(node); 16586 16587 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 16588 child = child.deleteData(0, 1); 16589 } 16590 16591 dom.remove(node, 1); 16592 } 16593 16594 selection.setRng(rng); 16595 } 16596 }; 16597 16598 // Applies formatting to the caret postion 16599 function applyCaretFormat() { 16600 var rng, caretContainer, textNode, offset, bookmark, container, text; 16601 16602 rng = selection.getRng(true); 16603 offset = rng.startOffset; 16604 container = rng.startContainer; 16605 text = container.nodeValue; 16606 16607 caretContainer = getParentCaretContainer(selection.getStart()); 16608 if (caretContainer) { 16609 textNode = findFirstTextNode(caretContainer); 16610 } 16611 16612 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 16613 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 16614 // Get bookmark of caret position 16615 bookmark = selection.getBookmark(); 16616 16617 // Collapse bookmark range (WebKit) 16618 rng.collapse(true); 16619 16620 // Expand the range to the closest word and split it at those points 16621 rng = expandRng(rng, get(name)); 16622 rng = rangeUtils.split(rng); 16623 16624 // Apply the format to the range 16625 apply(name, vars, rng); 16626 16627 // Move selection back to caret position 16628 selection.moveToBookmark(bookmark); 16629 } else { 16630 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 16631 caretContainer = createCaretContainer(true); 16632 textNode = caretContainer.firstChild; 16633 16634 rng.insertNode(caretContainer); 16635 offset = 1; 16636 16637 apply(name, vars, caretContainer); 16638 } else { 16639 apply(name, vars, caretContainer); 16640 } 16641 16642 // Move selection to text node 16643 selection.setCursorLocation(textNode, offset); 16644 } 16645 }; 16646 16647 function removeCaretFormat() { 16648 var rng = selection.getRng(true), container, offset, bookmark, 16649 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 16650 16651 container = rng.startContainer; 16652 offset = rng.startOffset; 16653 node = container; 16654 16655 if (container.nodeType == 3) { 16656 if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { 16657 hasContentAfter = true; 16658 } 16659 16660 node = node.parentNode; 16661 } 16662 16663 while (node) { 16664 if (matchNode(node, name, vars)) { 16665 formatNode = node; 16666 break; 16667 } 16668 16669 if (node.nextSibling) { 16670 hasContentAfter = true; 16671 } 16672 16673 parents.push(node); 16674 node = node.parentNode; 16675 } 16676 16677 // Node doesn't have the specified format 16678 if (!formatNode) { 16679 return; 16680 } 16681 16682 // Is there contents after the caret then remove the format on the element 16683 if (hasContentAfter) { 16684 // Get bookmark of caret position 16685 bookmark = selection.getBookmark(); 16686 16687 // Collapse bookmark range (WebKit) 16688 rng.collapse(true); 16689 16690 // Expand the range to the closest word and split it at those points 16691 rng = expandRng(rng, get(name), true); 16692 rng = rangeUtils.split(rng); 16693 16694 // Remove the format from the range 16695 remove(name, vars, rng); 16696 16697 // Move selection back to caret position 16698 selection.moveToBookmark(bookmark); 16699 } else { 16700 caretContainer = createCaretContainer(); 16701 16702 node = caretContainer; 16703 for (i = parents.length - 1; i >= 0; i--) { 16704 node.appendChild(dom.clone(parents[i], false)); 16705 node = node.firstChild; 16706 } 16707 16708 // Insert invisible character into inner most format element 16709 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 16710 node = node.firstChild; 16711 16712 // Insert caret container after the formated node 16713 dom.insertAfter(caretContainer, formatNode); 16714 16715 // Move selection to text node 16716 selection.setCursorLocation(node, 1); 16717 } 16718 }; 16719 16720 // Checks if the parent caret container node isn't empty if that is the case it 16721 // will remove the bogus state on all children that isn't empty 16722 function unmarkBogusCaretParents() { 16723 var i, caretContainer, node; 16724 16725 caretContainer = getParentCaretContainer(selection.getStart()); 16726 if (caretContainer && !dom.isEmpty(caretContainer)) { 16727 tinymce.walk(caretContainer, function(node) { 16728 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 16729 dom.setAttrib(node, 'data-mce-bogus', null); 16730 } 16731 }, 'childNodes'); 16732 } 16733 }; 16734 16735 // Only bind the caret events once 16736 if (!self._hasCaretEvents) { 16737 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 16738 ed.onBeforeGetContent.addToTop(function() { 16739 var nodes = [], i; 16740 16741 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 16742 // Mark children 16743 i = nodes.length; 16744 while (i--) { 16745 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 16746 } 16747 } 16748 }); 16749 16750 // Remove caret container on mouse up and on key up 16751 tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { 16752 ed[name].addToTop(function() { 16753 removeCaretContainer(); 16754 unmarkBogusCaretParents(); 16755 }); 16756 }); 16757 16758 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 16759 ed.onKeyDown.addToTop(function(ed, e) { 16760 var keyCode = e.keyCode; 16761 16762 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 16763 removeCaretContainer(getParentCaretContainer(selection.getStart())); 16764 } 16765 16766 unmarkBogusCaretParents(); 16767 }); 16768 16769 // Remove bogus state if they got filled by contents using editor.selection.setContent 16770 selection.onSetContent.add(unmarkBogusCaretParents); 16771 16772 self._hasCaretEvents = true; 16773 } 16774 16775 // Do apply or remove caret format 16776 if (type == "apply") { 16777 applyCaretFormat(); 16778 } else { 16779 removeCaretFormat(); 16780 } 16781 }; 16782 16783 function moveStart(rng) { 16784 var container = rng.startContainer, 16785 offset = rng.startOffset, isAtEndOfText, 16786 walker, node, nodes, tmpNode; 16787 16788 // Convert text node into index if possible 16789 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 16790 // Get the parent container location and walk from there 16791 offset = nodeIndex(container); 16792 container = container.parentNode; 16793 isAtEndOfText = true; 16794 } 16795 16796 // Move startContainer/startOffset in to a suitable node 16797 if (container.nodeType == 1) { 16798 nodes = container.childNodes; 16799 container = nodes[Math.min(offset, nodes.length - 1)]; 16800 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 16801 16802 // If offset is at end of the parent node walk to the next one 16803 if (offset > nodes.length - 1 || isAtEndOfText) 16804 walker.next(); 16805 16806 for (node = walker.current(); node; node = walker.next()) { 16807 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 16808 // IE has a "neat" feature where it moves the start node into the closest element 16809 // we can avoid this by inserting an element before it and then remove it after we set the selection 16810 tmpNode = dom.create('a', null, INVISIBLE_CHAR); 16811 node.parentNode.insertBefore(tmpNode, node); 16812 16813 // Set selection and remove tmpNode 16814 rng.setStart(node, 0); 16815 selection.setRng(rng); 16816 dom.remove(tmpNode); 16817 16818 return; 16819 } 16820 } 16821 } 16822 }; 16823 }; 16824 })(tinymce); 16825 16826 tinymce.onAddEditor.add(function(tinymce, ed) { 16827 var filters, fontSizes, dom, settings = ed.settings; 16828 16829 function replaceWithSpan(node, styles) { 16830 tinymce.each(styles, function(value, name) { 16831 if (value) 16832 dom.setStyle(node, name, value); 16833 }); 16834 16835 dom.rename(node, 'span'); 16836 }; 16837 16838 function convert(editor, params) { 16839 dom = editor.dom; 16840 16841 if (settings.convert_fonts_to_spans) { 16842 tinymce.each(dom.select('font,u,strike', params.node), function(node) { 16843 filters[node.nodeName.toLowerCase()](ed.dom, node); 16844 }); 16845 } 16846 }; 16847 16848 if (settings.inline_styles) { 16849 fontSizes = tinymce.explode(settings.font_size_legacy_values); 16850 16851 filters = { 16852 font : function(dom, node) { 16853 replaceWithSpan(node, { 16854 backgroundColor : node.style.backgroundColor, 16855 color : node.color, 16856 fontFamily : node.face, 16857 fontSize : fontSizes[parseInt(node.size, 10) - 1] 16858 }); 16859 }, 16860 16861 u : function(dom, node) { 16862 replaceWithSpan(node, { 16863 textDecoration : 'underline' 16864 }); 16865 }, 16866 16867 strike : function(dom, node) { 16868 replaceWithSpan(node, { 16869 textDecoration : 'line-through' 16870 }); 16871 } 16872 }; 16873 16874 ed.onPreProcess.add(convert); 16875 ed.onSetContent.add(convert); 16876 16877 ed.onInit.add(function() { 16878 ed.selection.onSetContent.add(convert); 16879 }); 16880 } 16881 }); 16882 16883 (function(tinymce) { 16884 var TreeWalker = tinymce.dom.TreeWalker; 16885 16886 tinymce.EnterKey = function(editor) { 16887 var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); 16888 16889 function handleEnterKey(evt) { 16890 var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, 16891 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 16892 16893 // Returns true if the block can be split into two blocks or not 16894 function canSplitBlock(node) { 16895 return node && 16896 dom.isBlock(node) && 16897 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 16898 !/^(fixed|absolute)/i.test(node.style.position) && 16899 dom.getContentEditable(node) !== "true"; 16900 }; 16901 16902 // Renders empty block on IE 16903 function renderBlockOnIE(block) { 16904 var oldRng; 16905 16906 if (tinymce.isIE && dom.isBlock(block)) { 16907 oldRng = selection.getRng(); 16908 block.appendChild(dom.create('span', null, '\u00a0')); 16909 selection.select(block); 16910 block.lastChild.outerHTML = ''; 16911 selection.setRng(oldRng); 16912 } 16913 }; 16914 16915 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 16916 function trimInlineElementsOnLeftSideOfBlock(block) { 16917 var node = block, firstChilds = [], i; 16918 16919 // Find inner most first child ex: <p><i><b>*</b></i></p> 16920 while (node = node.firstChild) { 16921 if (dom.isBlock(node)) { 16922 return; 16923 } 16924 16925 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16926 firstChilds.push(node); 16927 } 16928 } 16929 16930 i = firstChilds.length; 16931 while (i--) { 16932 node = firstChilds[i]; 16933 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 16934 dom.remove(node); 16935 } 16936 } 16937 }; 16938 16939 // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image 16940 function moveToCaretPosition(root) { 16941 var walker, node, rng, y, viewPort, lastNode = root, tempElm; 16942 16943 rng = dom.createRng(); 16944 16945 if (root.hasChildNodes()) { 16946 walker = new TreeWalker(root, root); 16947 16948 while (node = walker.current()) { 16949 if (node.nodeType == 3) { 16950 rng.setStart(node, 0); 16951 rng.setEnd(node, 0); 16952 break; 16953 } 16954 16955 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 16956 rng.setStartBefore(node); 16957 rng.setEndBefore(node); 16958 break; 16959 } 16960 16961 lastNode = node; 16962 node = walker.next(); 16963 } 16964 16965 if (!node) { 16966 rng.setStart(lastNode, 0); 16967 rng.setEnd(lastNode, 0); 16968 } 16969 } else { 16970 if (root.nodeName == 'BR') { 16971 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 16972 // Trick on older IE versions to render the caret before the BR between two lists 16973 if (!documentMode || documentMode < 9) { 16974 tempElm = dom.create('br'); 16975 root.parentNode.insertBefore(tempElm, root); 16976 } 16977 16978 rng.setStartBefore(root); 16979 rng.setEndBefore(root); 16980 } else { 16981 rng.setStartAfter(root); 16982 rng.setEndAfter(root); 16983 } 16984 } else { 16985 rng.setStart(root, 0); 16986 rng.setEnd(root, 0); 16987 } 16988 } 16989 16990 selection.setRng(rng); 16991 16992 // Remove tempElm created for old IE:s 16993 dom.remove(tempElm); 16994 16995 viewPort = dom.getViewPort(editor.getWin()); 16996 16997 // scrollIntoView seems to scroll the parent window in most browsers now including FF 3.0b4 so it's time to stop using it and do it our selfs 16998 y = dom.getPos(root).y; 16999 if (y < viewPort.y || y + 25 > viewPort.y + viewPort.h) { 17000 editor.getWin().scrollTo(0, y < viewPort.y ? y : y - viewPort.h + 25); // Needs to be hardcoded to roughly one line of text if a huge text block is broken into two blocks 17001 } 17002 }; 17003 17004 // Creates a new block element by cloning the current one or creating a new one if the name is specified 17005 // This function will also copy any text formatting from the parent block and add it to the new one 17006 function createNewBlock(name) { 17007 var node = container, block, clonedNode, caretNode; 17008 17009 block = name || parentBlockName == "TABLE" ? dom.create(name || newBlockName) : parentBlock.cloneNode(false); 17010 caretNode = block; 17011 17012 // Clone any parent styles 17013 if (settings.keep_styles !== false) { 17014 do { 17015 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(node.nodeName)) { 17016 clonedNode = node.cloneNode(false); 17017 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 17018 17019 if (block.hasChildNodes()) { 17020 clonedNode.appendChild(block.firstChild); 17021 block.appendChild(clonedNode); 17022 } else { 17023 caretNode = clonedNode; 17024 block.appendChild(clonedNode); 17025 } 17026 } 17027 } while (node = node.parentNode); 17028 } 17029 17030 // BR is needed in empty blocks on non IE browsers 17031 if (!tinymce.isIE) { 17032 caretNode.innerHTML = '<br>'; 17033 } 17034 17035 return block; 17036 }; 17037 17038 // Returns true/false if the caret is at the start/end of the parent block element 17039 function isCaretAtStartOrEndOfBlock(start) { 17040 var walker, node, name; 17041 17042 // Caret is in the middle of a text node like "a|b" 17043 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 17044 return false; 17045 } 17046 17047 // If after the last element in block node edge case for #5091 17048 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 17049 return true; 17050 } 17051 17052 // If the caret if before the first element in parentBlock 17053 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 17054 return true; 17055 } 17056 17057 // Caret can be before/after a table 17058 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 17059 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 17060 } 17061 17062 // Walk the DOM and look for text nodes or non empty elements 17063 walker = new TreeWalker(container, parentBlock); 17064 17065 // If caret is in beginning or end of a text block then jump to the next/previous node 17066 if (container.nodeType == 3) { 17067 if (start && offset == 0) { 17068 walker.prev(); 17069 } else if (!start && offset == container.nodeValue.length) { 17070 walker.next(); 17071 } 17072 } 17073 17074 while (node = walker.current()) { 17075 if (node.nodeType === 1) { 17076 // Ignore bogus elements 17077 if (!node.getAttribute('data-mce-bogus')) { 17078 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 17079 name = node.nodeName.toLowerCase(); 17080 if (nonEmptyElementsMap[name] && name !== 'br') { 17081 return false; 17082 } 17083 } 17084 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 17085 return false; 17086 } 17087 17088 if (start) { 17089 walker.prev(); 17090 } else { 17091 walker.next(); 17092 } 17093 } 17094 17095 return true; 17096 }; 17097 17098 // Wraps any text nodes or inline elements in the specified forced root block name 17099 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 17100 var newBlock, parentBlock, startNode, node, next, blockName = newBlockName || 'P'; 17101 17102 // Not in a block element or in a table cell or caption 17103 parentBlock = dom.getParent(container, dom.isBlock); 17104 if (!parentBlock || !canSplitBlock(parentBlock)) { 17105 parentBlock = parentBlock || editableRoot; 17106 17107 if (!parentBlock.hasChildNodes()) { 17108 newBlock = dom.create(blockName); 17109 parentBlock.appendChild(newBlock); 17110 rng.setStart(newBlock, 0); 17111 rng.setEnd(newBlock, 0); 17112 return newBlock; 17113 } 17114 17115 // Find parent that is the first child of parentBlock 17116 node = container; 17117 while (node.parentNode != parentBlock) { 17118 node = node.parentNode; 17119 } 17120 17121 // Loop left to find start node start wrapping at 17122 while (node && !dom.isBlock(node)) { 17123 startNode = node; 17124 node = node.previousSibling; 17125 } 17126 17127 if (startNode) { 17128 newBlock = dom.create(blockName); 17129 startNode.parentNode.insertBefore(newBlock, startNode); 17130 17131 // Start wrapping until we hit a block 17132 node = startNode; 17133 while (node && !dom.isBlock(node)) { 17134 next = node.nextSibling; 17135 newBlock.appendChild(node); 17136 node = next; 17137 } 17138 17139 // Restore range to it's past location 17140 rng.setStart(container, offset); 17141 rng.setEnd(container, offset); 17142 } 17143 } 17144 17145 return container; 17146 }; 17147 17148 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 17149 function handleEmptyListItem() { 17150 function isFirstOrLastLi(first) { 17151 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 17152 17153 // Find first/last element since there might be whitespace there 17154 while (node) { 17155 if (node.nodeType == 1) { 17156 break; 17157 } 17158 17159 node = node[first ? 'nextSibling' : 'previousSibling']; 17160 } 17161 17162 return node === parentBlock; 17163 }; 17164 17165 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 17166 17167 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 17168 // Is first and last list item then replace the OL/UL with a text block 17169 dom.replace(newBlock, containerBlock); 17170 } else if (isFirstOrLastLi(true)) { 17171 // First LI in list then remove LI and add text block before list 17172 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 17173 } else if (isFirstOrLastLi()) { 17174 // Last LI in list then temove LI and add text block after list 17175 dom.insertAfter(newBlock, containerBlock); 17176 renderBlockOnIE(newBlock); 17177 } else { 17178 // Middle LI in list the split the list and insert a text block in the middle 17179 // Extract after fragment and insert it after the current block 17180 tmpRng = rng.cloneRange(); 17181 tmpRng.setStartAfter(parentBlock); 17182 tmpRng.setEndAfter(containerBlock); 17183 fragment = tmpRng.extractContents(); 17184 dom.insertAfter(fragment, containerBlock); 17185 dom.insertAfter(newBlock, containerBlock); 17186 } 17187 17188 dom.remove(parentBlock); 17189 moveToCaretPosition(newBlock); 17190 undoManager.add(); 17191 }; 17192 17193 // Walks the parent block to the right and look for BR elements 17194 function hasRightSideBr() { 17195 var walker = new TreeWalker(container, parentBlock), node; 17196 17197 while (node = walker.current()) { 17198 if (node.nodeName == 'BR') { 17199 return true; 17200 } 17201 17202 node = walker.next(); 17203 } 17204 } 17205 17206 // Inserts a BR element if the forced_root_block option is set to false or empty string 17207 function insertBr() { 17208 var brElm, extraBr; 17209 17210 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 17211 // Insert extra BR element at the end block elements 17212 if (!tinymce.isIE && !hasRightSideBr()) { 17213 brElm = dom.create('br') 17214 rng.insertNode(brElm); 17215 rng.setStartAfter(brElm); 17216 rng.setEndAfter(brElm); 17217 extraBr = true; 17218 } 17219 } 17220 17221 brElm = dom.create('br'); 17222 rng.insertNode(brElm); 17223 17224 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 17225 if (tinymce.isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 17226 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 17227 } 17228 17229 if (!extraBr) { 17230 rng.setStartAfter(brElm); 17231 rng.setEndAfter(brElm); 17232 } else { 17233 rng.setStartBefore(brElm); 17234 rng.setEndBefore(brElm); 17235 } 17236 17237 selection.setRng(rng); 17238 undoManager.add(); 17239 }; 17240 17241 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 17242 function trimLeadingLineBreaks(node) { 17243 do { 17244 if (node.nodeType === 3) { 17245 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 17246 } 17247 17248 node = node.firstChild; 17249 } while (node); 17250 }; 17251 17252 function getEditableRoot(node) { 17253 var root = dom.getRoot(), parent, editableRoot; 17254 17255 // Get all parents until we hit a non editable parent or the root 17256 parent = node; 17257 while (parent !== root && dom.getContentEditable(parent) !== "false") { 17258 if (dom.getContentEditable(parent) === "true") { 17259 editableRoot = parent; 17260 } 17261 17262 parent = parent.parentNode; 17263 } 17264 17265 return parent !== root ? editableRoot : root; 17266 }; 17267 17268 // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block 17269 function addBrToBlockIfNeeded(block) { 17270 var lastChild; 17271 17272 // IE will render the blocks correctly other browsers needs a BR 17273 if (!tinymce.isIE) { 17274 block.normalize(); // Remove empty text nodes that got left behind by the extract 17275 17276 // Check if the block is empty or contains a floated last child 17277 lastChild = block.lastChild; 17278 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 17279 dom.add(block, 'br'); 17280 } 17281 } 17282 }; 17283 17284 // Delete any selected contents 17285 if (!rng.collapsed) { 17286 editor.execCommand('Delete'); 17287 return; 17288 } 17289 17290 // Event is blocked by some other handler for example the lists plugin 17291 if (evt.isDefaultPrevented()) { 17292 return; 17293 } 17294 17295 // Setup range items and newBlockName 17296 container = rng.startContainer; 17297 offset = rng.startOffset; 17298 newBlockName = settings.forced_root_block; 17299 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 17300 documentMode = dom.doc.documentMode; 17301 17302 // Resolve node index 17303 if (container.nodeType == 1 && container.hasChildNodes()) { 17304 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 17305 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 17306 if (isAfterLastNodeInContainer && container.nodeType == 3) { 17307 offset = container.nodeValue.length; 17308 } else { 17309 offset = 0; 17310 } 17311 } 17312 17313 // Get editable root node normaly the body element but sometimes a div or span 17314 editableRoot = getEditableRoot(container); 17315 17316 // If there is no editable root then enter is done inside a contentEditable false element 17317 if (!editableRoot) { 17318 return; 17319 } 17320 17321 undoManager.beforeChange(); 17322 17323 // If editable root isn't block nor the root of the editor 17324 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 17325 if (!newBlockName || evt.shiftKey) { 17326 insertBr(); 17327 } 17328 17329 return; 17330 } 17331 17332 // Wrap the current node and it's sibling in a default block if it's needed. 17333 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 17334 // This won't happen if root blocks are disabled or the shiftKey is pressed 17335 if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) { 17336 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 17337 } 17338 17339 // Find parent block and setup empty block paddings 17340 parentBlock = dom.getParent(container, dom.isBlock); 17341 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 17342 17343 // Setup block names 17344 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17345 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17346 17347 // Handle enter inside an empty list item 17348 if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { 17349 // Let the list plugin or browser handle nested lists for now 17350 if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { 17351 return false; 17352 } 17353 17354 handleEmptyListItem(); 17355 return; 17356 } 17357 17358 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 17359 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 17360 if (!evt.shiftKey) { 17361 insertBr(); 17362 return; 17363 } 17364 } else { 17365 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 17366 if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { 17367 insertBr(); 17368 return; 17369 } 17370 } 17371 17372 // Default block name if it's not configured 17373 newBlockName = newBlockName || 'P'; 17374 17375 // Insert new block before/after the parent block depending on caret location 17376 if (isCaretAtStartOrEndOfBlock()) { 17377 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 17378 if (/^(H[1-6]|PRE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 17379 newBlock = createNewBlock(newBlockName); 17380 } else { 17381 newBlock = createNewBlock(); 17382 } 17383 17384 // Split the current container block element if enter is pressed inside an empty inner block element 17385 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 17386 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 17387 newBlock = dom.split(containerBlock, parentBlock); 17388 } else { 17389 dom.insertAfter(newBlock, parentBlock); 17390 } 17391 17392 moveToCaretPosition(newBlock); 17393 } else if (isCaretAtStartOrEndOfBlock(true)) { 17394 // Insert new block before 17395 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 17396 renderBlockOnIE(newBlock); 17397 } else { 17398 // Extract after fragment and insert it after the current block 17399 tmpRng = rng.cloneRange(); 17400 tmpRng.setEndAfter(parentBlock); 17401 fragment = tmpRng.extractContents(); 17402 trimLeadingLineBreaks(fragment); 17403 newBlock = fragment.firstChild; 17404 dom.insertAfter(fragment, parentBlock); 17405 trimInlineElementsOnLeftSideOfBlock(newBlock); 17406 addBrToBlockIfNeeded(parentBlock); 17407 moveToCaretPosition(newBlock); 17408 } 17409 17410 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 17411 undoManager.add(); 17412 } 17413 17414 editor.onKeyDown.add(function(ed, evt) { 17415 if (evt.keyCode == 13) { 17416 if (handleEnterKey(evt) !== false) { 17417 evt.preventDefault(); 17418 } 17419 } 17420 }); 17421 }; 17422 })(tinymce); 17423 17424